From 4182d918a684d33864549ec2550cea783c3b5848 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Tue, 18 Feb 2025 11:38:55 +0100 Subject: [PATCH] feat: Add extension for FileManager to track file I/O operations with Sentry --- CHANGELOG.md | 3 + Sentry.xcodeproj/project.pbxproj | 20 + .../IO/FileManager+SentryTracing.swift | 140 +++ .../IO/SentryFileIOTracker+SwiftHelpers.swift | 174 ++++ .../FileManagerTracingIntegrationTests.swift | 855 ++++++++++++++++++ 5 files changed, 1192 insertions(+) create mode 100644 Sources/Swift/Integrations/Performance/IO/FileManager+SentryTracing.swift create mode 100644 Sources/Swift/Integrations/Performance/IO/SentryFileIOTracker+SwiftHelpers.swift create mode 100644 Tests/SentryTests/Integrations/Performance/IO/FileManagerTracingIntegrationTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index a8f4e5926e9..c65a2a478b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ ### Improvements - Add experimental flag to disable swizzling of `NSData` individually (#4859) +### Features + +- Add extension for `FileManager` to track file I/O operations with Sentry (#4862) ## 8.45.0 diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index ca19980c212..30bf19dd814 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -813,6 +813,9 @@ D42E48572D48DF1600D251BC /* SentryBuildAppStartSpansTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D42E48562D48DF1600D251BC /* SentryBuildAppStartSpansTests.swift */; }; D43647EF2D5CF9E3001468E0 /* SentrySpanKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = D43647EE2D5CF9DC001468E0 /* SentrySpanKey.swift */; }; D43647F12D5CFB71001468E0 /* SentrySpanKeyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D43647F02D5CFB71001468E0 /* SentrySpanKeyTests.swift */; }; + D43647AB2D5CAA32001468E0 /* FileManager+SentryTracing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D43647AA2D5CAA32001468E0 /* FileManager+SentryTracing.swift */; }; + D43647F32D5CFBC7001468E0 /* FileManagerTracingIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D43647F22D5CFBC2001468E0 /* FileManagerTracingIntegrationTests.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 */; }; @@ -1953,6 +1956,9 @@ D42E48582D48FC8F00D251BC /* SentryNSDictionarySanitizeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryNSDictionarySanitizeTests.swift; sourceTree = ""; }; D43647EE2D5CF9DC001468E0 /* SentrySpanKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySpanKey.swift; sourceTree = ""; }; D43647F02D5CFB71001468E0 /* SentrySpanKeyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SentrySpanKeyTests.swift; sourceTree = ""; }; + D43647AA2D5CAA32001468E0 /* FileManager+SentryTracing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+SentryTracing.swift"; sourceTree = ""; }; + D43647F22D5CFBC2001468E0 /* FileManagerTracingIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManagerTracingIntegrationTests.swift; sourceTree = ""; }; + D468C0612D3669A200964230 /* SentryFileIOTracker+SwiftHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SentryFileIOTracker+SwiftHelpers.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 = ""; }; @@ -3784,6 +3790,16 @@ name = Transaction; sourceTree = ""; }; + D468C0602D36699700964230 /* IO */ = { + isa = PBXGroup; + children = ( + D4EDF9832D0B2A1D0071E7B3 /* Data+SentryTracing.swift */, + D43647AA2D5CAA32001468E0 /* FileManager+SentryTracing.swift */, + D468C0612D3669A200964230 /* SentryFileIOTracker+SwiftHelpers.swift */, + ); + path = IO; + sourceTree = ""; + }; D48724D92D35258A005DE483 /* Transactions */ = { isa = PBXGroup; children = ( @@ -4015,6 +4031,8 @@ D875ED09276CC83200422FAC /* IO */ = { isa = PBXGroup; children = ( + D43647F22D5CFBC2001468E0 /* FileManagerTracingIntegrationTests.swift */, + D4C5F5992D4249E0002A9BF6 /* DataSentryTracingIntegrationTests.swift */, D875ED0A276CC84700422FAC /* SentryFileIOTrackerTests.swift */, D8CE69BB277E39C700C6EC5C /* SentryFileIOTrackingIntegrationObjCTests.m */, D885266327739D01001269FC /* SentryFileIOTrackingIntegrationTests.swift */, @@ -4943,6 +4961,7 @@ 843FB3232D0CD04D00558F18 /* SentryUserAccess.m in Sources */, 63FE716720DA4C1100CDBAE8 /* SentryCrashCPU.c in Sources */, 63FE717320DA4C1100CDBAE8 /* SentryCrashC.c in Sources */, + D43647AB2D5CAA32001468E0 /* FileManager+SentryTracing.swift in Sources */, 6293F5752D422A95002BC3BD /* SentryStacktraceCodable.swift in Sources */, 63FE712120DA4C1000CDBAE8 /* SentryCrashSymbolicator.c in Sources */, 627C77892D50B6840055E966 /* SentryBreadcrumbCodable.swift in Sources */, @@ -5348,6 +5367,7 @@ 8F73BC312B02B87E00C3CEF4 /* SentryInstallationTests.swift in Sources */, 7B569E002590EEF600B653FC /* SentryScope+Equality.m in Sources */, D8BFE37929A76666002E73F3 /* SentryTimeToDisplayTrackerTest.swift in Sources */, + D43647F32D5CFBC7001468E0 /* FileManagerTracingIntegrationTests.swift in Sources */, D84541182A2DC2CD00E2B11C /* SentryBinaryImageCacheTests.swift in Sources */, 7BF536D424BEF255004FA6A2 /* SentryAssertions.swift in Sources */, 7BC6EC14255C415E0059822A /* SentryExceptionTests.swift in Sources */, diff --git a/Sources/Swift/Integrations/Performance/IO/FileManager+SentryTracing.swift b/Sources/Swift/Integrations/Performance/IO/FileManager+SentryTracing.swift new file mode 100644 index 00000000000..a95aa118101 --- /dev/null +++ b/Sources/Swift/Integrations/Performance/IO/FileManager+SentryTracing.swift @@ -0,0 +1,140 @@ +@_implementationOnly import _SentryPrivate +import Foundation + +/// A ``FileManager`` extension that tracks read and write operations with Sentry. +/// +/// - Note: Methods provided by this extension reflect the same functionality as the original ``FileManager`` methods, but they track the operation with Sentry. +public extension FileManager { + + // MARK: - Creating and Deleting Items + + /// Creates a file with the specified content and attributes at the given 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.enableSwizzling` to `false` when initializing Sentry. + /// - Parameters: + /// - path: The path for the new file. + /// - data: A data object containing the contents of the new file. + /// - attr: A dictionary containing the attributes to associate with the new file. + /// You can use these attributes to set the owner and group numbers, file permissions, and modification date. + /// For a list of keys, see ``FileAttributeKey``. If you specify `nil` for attributes, the file is created with a set of default attributes. + /// - Returns: `true` if the operation was successful or if the item already exists, otherwise `false`. + /// - Note: See ``FileManager.createFile(atPath:contents:attributes:)`` for more information. + func createFileWithSentryTracing(atPath path: String, contents data: Data?, attributes attr: [FileAttributeKey: Any]? = nil) -> Bool { + let tracker = SentryFileIOTracker.sharedInstance() + return tracker + .measureCreatingFile( + atPath: path, + contents: data, + attributes: attr, + origin: SentryTraceOrigin.manualFileData) { path, data, attr in + self.createFile(atPath: path, contents: data, attributes: attr) + } + } + + /// Removes the file or directory at the specified 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.enableSwizzling` to `false` when initializing Sentry. + /// - Parameter url: A file URL specifying the file or directory to remove. + /// If the URL specifies a directory, the contents of that directory are recursively removed. + /// - Note: See ``FileManager.removeItem(at:)`` for more information. + func removeItemWithSentryTracing(at url: URL) throws { + let tracker = SentryFileIOTracker.sharedInstance() + try tracker.measureRemovingItem(at: url, origin: SentryTraceOrigin.manualFileData) { url in + try self.removeItem(at: url) + } + } + + /// Removes the file or directory at the specified path, 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.enableSwizzling` to `false` when initializing Sentry. + /// - Parameter path: A path string indicating the file or directory to remove. + /// If the path specifies a directory, the contents of that directory are recursively removed. + /// - Note: See ``FileManager.removeItem(atPath:)`` for more information. + func removeItemWithSentryTracing(atPath path: String) throws { + let tracker = SentryFileIOTracker.sharedInstance() + try tracker.measureRemovingItem(atPath: path, origin: SentryTraceOrigin.manualFileData) { path in + try self.removeItem(atPath: path) + } + } + + // MARK: - Moving and Copying Items + + /// Copies the file at the specified URL to a new location synchronously, 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.enableSwizzling` to `false` when initializing Sentry. + /// - Parameters: + /// - srcURL: The file URL that identifies the file you want to copy. + /// The URL in this parameter must not be a file reference URL. + /// - dstURL: The URL at which to place the copy of `srcURL`. + /// The URL in this parameter must not be a file reference URL and must include the name of the file in its new location. + /// - Note: See ``FileManager.copyItem(at:to:)`` for more information. + func copyItemWithSentryTracing(at srcURL: URL, to dstURL: URL) throws { + let tracker = SentryFileIOTracker.sharedInstance() + try tracker.measureCopyingItem(at: srcURL, to: dstURL, origin: SentryTraceOrigin.manualFileData) { srcURL, dstURL in + try self.copyItem(at: srcURL, to: dstURL) + } + } + + /// Copies the item at the specified path to a new location synchronously, 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.enableSwizzling` to `false` when initializing Sentry. + /// - Parameters: + /// - srcPath: The path to the file or directory you want to move. + /// - dstPath: The path at which to place the copy of `srcPath`. + /// This path must include the name of the file or directory in its new location. + /// - Note: See ``FileManager.copyItem(atPath:toPath:)`` for more information. + func copyItemWithSentryTracing(at srcPath: String, to dstPath: String) throws { + let tracker = SentryFileIOTracker.sharedInstance() + try tracker.measureCopyingItem(atPath: srcPath, toPath: dstPath, origin: SentryTraceOrigin.manualFileData) { srcPath, dstPath in + try self.copyItem(atPath: srcPath, toPath: dstPath) + } + } + + /// Moves the file or directory at the specified URL to a new location synchronously, 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.enableSwizzling` to `false` when initializing Sentry. + /// - Parameters: + /// - srcURL: The file URL that identifies the file or directory you want to move. + /// The URL in this parameter must not be a file reference URL. + /// - dstURL: The new location for the item in `srcURL`. + /// The URL in this parameter must not be a file reference URL and must include the name of the file or directory in its new location. + /// - Note: See ``FileManager.moveItem(at:to:)`` for more information. + func moveItemWithSentryTracing(at srcURL: URL, to dstURL: URL) throws { + let tracker = SentryFileIOTracker.sharedInstance() + try tracker.measureMovingItem( + at: srcURL, + to: dstURL, + origin: SentryTraceOrigin.manualFileData) { srcURL, dstURL in + try self.moveItem(at: srcURL, to: dstURL) + } + } + + /// Moves the file or directory at the specified path to a new location synchronously, 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.enableSwizzling` to `false` when initializing Sentry. + /// - Parameters: + /// - srcPath: The path to the file or directory you want to move. + /// - dstPath: The new path for the item in `srcPath`. + /// This path must include the name of the file or directory in its new location. + /// - Note: See ``FileManager.moveItem(atPath:toPath:)`` for more information. + func moveItemWithSentryTracing(at srcPath: String, to dstPath: String) throws { + let tracker = SentryFileIOTracker.sharedInstance() + try tracker.measureMovingItem(atPath: srcPath, toPath: dstPath, origin: SentryTraceOrigin.manualFileData) { srcPath, dstPath in + try self.moveItem(atPath: srcPath, toPath: dstPath) + } + } +} 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..80cf4945ce0 --- /dev/null +++ b/Sources/Swift/Integrations/Performance/IO/SentryFileIOTracker+SwiftHelpers.swift @@ -0,0 +1,174 @@ +@_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) + } + defer { + span.finish() + } + let data = try method(url, options) + span.setData(value: data.count, key: SentrySpanKey.fileSize) + return data + } + + 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) + } + defer { + span.finish() + } + try method(data, url, options) + } + + func measureRemovingItem( + at url: URL, + origin: String, + method: (_ url: URL) 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(url) + } + guard let span = self.span(forPath: url.path, origin: origin, operation: SentrySpanOperation.fileDelete) else { + return try method(url) + } + defer { + span.finish() + } + try method(url) + } + + func measureRemovingItem( + atPath path: String, + origin: String, + method: (_ path: String) throws -> Void + ) rethrows { + guard let span = self.span(forPath: path, origin: origin, operation: SentrySpanOperation.fileDelete) else { + return try method(path) + } + defer { + span.finish() + } + try method(path) + } + + func measureCreatingFile( + atPath path: String, + contents data: Data?, + attributes attr: [FileAttributeKey: Any]?, + origin: String, + method: (_ path: String, _ data: Data?, _ attributes: [FileAttributeKey: Any]?) -> Bool + ) -> Bool { + let size = UInt(data?.count ?? 0) + guard let span = self.span(forPath: path, origin: origin, operation: SentrySpanOperation.fileWrite, size: size) else { + return method(path, data, attr) + } + defer { + if let data = data { + span.setData(value: data.count, key: SentrySpanKey.fileSize) + } + span.finish() + } + return method(path, data, attr) + } + + func measureCopyingItem( + at srcUrl: URL, + to dstUrl: URL, + origin: String, + method: (_ srcUrl: URL, _ dstUrl: URL) 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 srcUrl.scheme == NSURLFileScheme && dstUrl.scheme == NSURLFileScheme else { + return try method(srcUrl, dstUrl) + } + guard let span = self.span(forPath: srcUrl.path, origin: origin, operation: SentrySpanOperation.fileCopy) else { + return try method(srcUrl, dstUrl) + } + defer { + span.finish() + } + try method(srcUrl, dstUrl) + } + + func measureCopyingItem( + atPath srcPath: String, + toPath dstPath: String, + origin: String, + method: (_ srcPath: String, _ dstPath: String) throws -> Void + ) rethrows { + guard let span = self.span(forPath: srcPath, origin: origin, operation: SentrySpanOperation.fileCopy) else { + return try method(srcPath, dstPath) + } + defer { + span.finish() + } + try method(srcPath, dstPath) + } + + func measureMovingItem( + at srcUrl: URL, + to dstUrl: URL, + origin: String, + method: (_ srcUrl: URL, _ dstUrl: URL) 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 srcUrl.scheme == NSURLFileScheme && dstUrl.scheme == NSURLFileScheme else { + return try method(srcUrl, dstUrl) + } + guard let span = self.span(forPath: srcUrl.path, origin: origin, operation: SentrySpanOperation.fileRename) else { + return try method(srcUrl, dstUrl) + } + defer { + span.finish() + } + try method(srcUrl, dstUrl) + } + + func measureMovingItem( + atPath srcPath: String, + toPath dstPath: String, + origin: String, + method: (_ srcPath: String, _ dstPath: String) throws -> Void + ) rethrows { + guard let span = self.span(forPath: srcPath, origin: origin, operation: SentrySpanOperation.fileRename) else { + return try method(srcPath, dstPath) + } + defer { + span.finish() + } + try method(srcPath, dstPath) + } +} diff --git a/Tests/SentryTests/Integrations/Performance/IO/FileManagerTracingIntegrationTests.swift b/Tests/SentryTests/Integrations/Performance/IO/FileManagerTracingIntegrationTests.swift new file mode 100644 index 00000000000..03868021fd9 --- /dev/null +++ b/Tests/SentryTests/Integrations/Performance/IO/FileManagerTracingIntegrationTests.swift @@ -0,0 +1,855 @@ +@testable import Sentry +import SentryTestUtils +import XCTest + +class FileManagerSentryTracingIntegrationTests: XCTestCase { + private class Fixture { + + let data = "SOME DATA".data(using: .utf8)! + + var fileSrcUrl: URL! + var fileDestUrl: URL! + var ignoredFileToDeleteUrl: URL! + var ignoredFileToCreateUrl: URL! + var ignoredSrcFileUrl: URL! + + init() {} + + func getSut(testName: String, isEnabled: Bool = true) throws -> FileManager { + let tempDir = URL(fileURLWithPath: NSTemporaryDirectory()) + .appendingPathComponent("test-\(testName.hashValue.description)") + try! FileManager.default + .createDirectory(at: tempDir, withIntermediateDirectories: true) + + fileSrcUrl = tempDir.appendingPathComponent("source-file") + try data.write(to: fileSrcUrl) + + fileDestUrl = tempDir.appendingPathComponent("destination-file") + + // Initialize the SDK after files are written, so preparations are not traced + SentrySDK.start { options in + 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 = false + + // Configure the cache directory to a temporary directory, so we can isolate the test files + options.cacheDirectoryPath = tempDir.path + } + + // Get the working directory of the SDK, as these files are ignored by default + let sentryPath = SentrySDK.currentHub().getClient()!.fileManager.sentryPath + ignoredFileToCreateUrl = URL(fileURLWithPath: sentryPath).appendingPathComponent("ignored-file-to-create") + + ignoredFileToDeleteUrl = URL(fileURLWithPath: sentryPath).appendingPathComponent("ignored-file-to-delete") + try data.write(to: ignoredFileToDeleteUrl) + + ignoredSrcFileUrl = URL(fileURLWithPath: sentryPath).appendingPathComponent("ignored-src-file") + try data.write(to: ignoredSrcFileUrl) + + return FileManager.default + } + + var fileSrcPath: String { fileSrcUrl.path } + var invalidSrcUrl: URL { URL(fileURLWithPath: "/path/that/does/not/exist") } + var invalidSrcPath: String { invalidSrcUrl.path } + var ignoredSrcFilePath: String { ignoredSrcFileUrl.path } + + var fileDestPath: String { fileDestUrl.path } + var invalidDestUrl: URL { URL(fileURLWithPath: "/path/that/does/not/exist") } + var invalidDestPath: String { invalidDestUrl.path } + + var fileUrlToDelete: URL { fileSrcUrl } + var filePathToDelete: String { fileUrlToDelete.path } + var invalidUrlToDelete: URL { invalidSrcUrl } + var invalidPathToDelete: String { invalidSrcPath } + + var filePathToCreate: String { fileDestUrl.path } + var invalidPathToCreate: String { invalidDestPath } + + 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")! + } + + var ignoredFileToCreatePath: String { + ignoredFileToCreateUrl.path + } + + var ignoredFileToDeletePath: String { + ignoredFileToDeleteUrl.path + } + } + + private var fixture: Fixture! + + override func setUp() { + super.setUp() + fixture = Fixture() + } + + override func tearDown() { + super.tearDown() + clearTestState() + } + + // MARK: - FileManager.createFileWithSentryTracing(atPath:contents:attributes:) + + func testCreateFileAtPathWithSentryTracing_withoutData_shouldTraceManually() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // Check pre-condition + var isFileCreated = FileManager.default.fileExists(atPath: fixture.filePathToCreate) + XCTAssertFalse(isFileCreated) + + // -- Act -- + let result = sut.createFileWithSentryTracing(atPath: fixture.filePathToCreate, contents: nil) + + // -- Assert -- + XCTAssertTrue(result) + + isFileCreated = FileManager.default.fileExists(atPath: fixture.filePathToCreate) + XCTAssertTrue(isFileCreated) + + let writtenData = try Data(contentsOf: fixture.fileDestUrl) + XCTAssertEqual(writtenData.count, 0) + + XCTAssertEqual(parentTransaction.children.count, 1) + let span = try XCTUnwrap(parentTransaction.children.first) + XCTAssertEqual(span.origin, SentryTraceOrigin.manualFileData) + XCTAssertEqual(span.operation, SentrySpanOperation.fileWrite) + XCTAssertEqual(span.data["file.path"] as? String, fixture.filePathToCreate) + XCTAssertNil(span.data["file.size"]) + } + + func testCreateFileAtPathWithSentryTracing_withData_shouldTraceManually() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // Check pre-condition + var isFileCreated = FileManager.default.fileExists(atPath: fixture.filePathToCreate) + XCTAssertFalse(isFileCreated) + + // -- Act -- + let result = sut.createFileWithSentryTracing(atPath: fixture.filePathToCreate, contents: fixture.data) + + // -- Assert -- + XCTAssertTrue(result) + + isFileCreated = FileManager.default.fileExists(atPath: fixture.filePathToCreate) + XCTAssertTrue(isFileCreated) + + let writtenData = try Data(contentsOf: fixture.fileDestUrl) + XCTAssertEqual(writtenData, fixture.data) + + XCTAssertEqual(parentTransaction.children.count, 1) + let span = try XCTUnwrap(parentTransaction.children.first) + XCTAssertEqual(span.origin, SentryTraceOrigin.manualFileData) + XCTAssertEqual(span.operation, SentrySpanOperation.fileWrite) + XCTAssertEqual(span.data["file.path"] as? String, fixture.filePathToCreate) + XCTAssertEqual(span.data["file.size"] as? Int, fixture.data.count) + } + + func testCreateFileAtPathWithSentryTracing_failsToCreateFile_shouldTraceManually() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // Check pre-condition + var isFileCreated = FileManager.default.fileExists(atPath: fixture.invalidPathToCreate) + XCTAssertFalse(isFileCreated) + + // -- Act -- + let result = sut.createFileWithSentryTracing(atPath: fixture.invalidPathToCreate, contents: fixture.data) + + // -- Assert -- + XCTAssertFalse(result) + isFileCreated = FileManager.default.fileExists(atPath: fixture.filePathToCreate) + XCTAssertFalse(isFileCreated) + + XCTAssertEqual(parentTransaction.children.count, 1) + let span = try XCTUnwrap(parentTransaction.children.first) + XCTAssertEqual(span.origin, SentryTraceOrigin.manualFileData) + XCTAssertEqual(span.operation, SentrySpanOperation.fileWrite) + XCTAssertEqual(span.data["file.path"] as? String, fixture.invalidPathToCreate) + XCTAssertEqual(span.data["file.size"] as? Int, fixture.data.count) + } + + func testCreateFileAtPathWithSentryTracing_trackerIsNotEnabled_shouldNotTraceManually() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name, isEnabled: false) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + var isFileCreated = FileManager.default.fileExists(atPath: fixture.filePathToCreate) + XCTAssertFalse(isFileCreated) + + // -- Act -- + let result = sut.createFileWithSentryTracing(atPath: fixture.filePathToCreate, contents: fixture.data) + + // -- Assert -- + XCTAssertTrue(result) + isFileCreated = FileManager.default.fileExists(atPath: fixture.filePathToCreate) + XCTAssertTrue(isFileCreated) + let writtenData = try Data(contentsOf: fixture.fileDestUrl) + XCTAssertEqual(writtenData, fixture.data) + + XCTAssertEqual(parentTransaction.children.count, 0) + } + + func testCreateFileAtPathWithSentryTracing_fileIsIgnored_shouldNotTraceManually() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + var isFileCreated = FileManager.default.fileExists(atPath: fixture.ignoredFileToCreatePath) + XCTAssertFalse(isFileCreated) + + // -- Act -- + let result = sut.createFileWithSentryTracing(atPath: fixture.ignoredFileToCreatePath, contents: fixture.data) + + // -- Assert -- + XCTAssertTrue(result) + isFileCreated = FileManager.default.fileExists(atPath: fixture.ignoredFileToCreatePath) + XCTAssertTrue(isFileCreated) + let writtenData = try Data(contentsOf: fixture.ignoredFileToCreateUrl) + XCTAssertEqual(writtenData, fixture.data) + + XCTAssertEqual(parentTransaction.children.count, 0) + } + + // MARK: - FileManager.removeItemWithSentryTracing(at:) + + func testRemoveItemAtUrlWithSentryTracing_shouldTraceManually() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // Check pre-condition + var isFileRemoved = !FileManager.default.fileExists(atPath: fixture.filePathToDelete) + XCTAssertFalse(isFileRemoved) + + // -- Act -- + try sut.removeItemWithSentryTracing(at: fixture.fileUrlToDelete) + + // -- Assert -- + isFileRemoved = !FileManager.default.fileExists(atPath: fixture.filePathToDelete) + XCTAssertTrue(isFileRemoved) + + XCTAssertEqual(parentTransaction.children.count, 1) + let span = try XCTUnwrap(parentTransaction.children.first) + XCTAssertEqual(span.origin, SentryTraceOrigin.manualFileData) + XCTAssertEqual(span.operation, SentrySpanOperation.fileDelete) + XCTAssertEqual(span.data["file.path"] as? String, fixture.fileUrlToDelete.path) + } + + func testRemoveItemAtUrlWithSentryTracing_throwsError_shouldTraceManuallyWithErrorRethrow() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // -- Act & Assert -- + XCTAssertThrowsError(try sut.removeItemWithSentryTracing(at: fixture.invalidUrlToDelete)) + + XCTAssertEqual(parentTransaction.children.count, 1) + let span = try XCTUnwrap(parentTransaction.children.first) + XCTAssertEqual(span.origin, SentryTraceOrigin.manualFileData) + XCTAssertEqual(span.operation, SentrySpanOperation.fileDelete) + XCTAssertEqual(span.data["file.path"] as? String, fixture.invalidPathToDelete) + } + + func testRemoveItemAtUrlWithSentryTracing_nonFileUrl_shouldNotTraceManually() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // -- Act -- + XCTAssertThrowsError(try sut.removeItemWithSentryTracing(at: fixture.nonFileUrl)) + + // -- Assert -- + XCTAssertEqual(parentTransaction.children.count, 0) + } + + func testRemoveItemAtUrlWithSentryTracing_trackerIsNotEnabled_shouldNotTraceManually() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name, isEnabled: false) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // Check-precondition + var isFileRemoved = !FileManager.default.fileExists(atPath: fixture.filePathToDelete) + XCTAssertFalse(isFileRemoved) + + // -- Act -- + try sut.removeItemWithSentryTracing(at: fixture.fileUrlToDelete) + + // -- Assert -- + isFileRemoved = !FileManager.default.fileExists(atPath: fixture.filePathToDelete) + XCTAssertTrue(isFileRemoved) + + XCTAssertEqual(parentTransaction.children.count, 0) + } + + func testRemoveItemAtUrlWithSentryTracing_fileIsIgnored_shouldNotTraceManually() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // Check pre-conditions + var isFileRemoved = !FileManager.default.fileExists(atPath: fixture.ignoredFileToDeleteUrl.path) + XCTAssertFalse(isFileRemoved) + + // -- Act -- + try sut.removeItemWithSentryTracing(at: fixture.ignoredFileToDeleteUrl) + + // -- Assert -- + isFileRemoved = !FileManager.default.fileExists(atPath: fixture.ignoredFileToDeleteUrl.path) + XCTAssertTrue(isFileRemoved) + + XCTAssertEqual(parentTransaction.children.count, 0) + } + + // MARK: - FileManager.removeItemWithSentryTracing(atPath:) + + func testRemoveItemAtPathWithSentryTracing_shouldTraceManually() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // Smoke test to ensure the file is written + var isFileRemoved = !FileManager.default.fileExists(atPath: fixture.filePathToDelete) + XCTAssertFalse(isFileRemoved) + + // -- Act -- + try sut.removeItemWithSentryTracing(atPath: fixture.filePathToDelete) + + // -- Assert -- + isFileRemoved = !FileManager.default.fileExists(atPath: fixture.filePathToDelete) + XCTAssertTrue(isFileRemoved) + + XCTAssertEqual(parentTransaction.children.count, 1) + let span = try XCTUnwrap(parentTransaction.children.first) + XCTAssertEqual(span.origin, SentryTraceOrigin.manualFileData) + XCTAssertEqual(span.operation, SentrySpanOperation.fileDelete) + XCTAssertEqual(span.data["file.path"] as? String, fixture.fileSrcPath) + } + + func testRemoveItemAtPathWithSentryTracing_throwsError_shouldTraceManuallyWithErrorRethrow() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // -- Act & Assert -- + XCTAssertThrowsError(try sut.removeItemWithSentryTracing(atPath: fixture.invalidPathToDelete)) + + XCTAssertEqual(parentTransaction.children.count, 1) + let span = try XCTUnwrap(parentTransaction.children.first) + XCTAssertEqual(span.origin, SentryTraceOrigin.manualFileData) + XCTAssertEqual(span.operation, SentrySpanOperation.fileDelete) + XCTAssertEqual(span.data["file.path"] as? String, fixture.invalidSrcPath) + } + + func testRemoveItemAtPathWithSentryTracing_trackerIsNotEnabled_shouldNotTraceManually() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name, isEnabled: false) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + var isFileRemoved = !FileManager.default.fileExists(atPath: fixture.filePathToDelete) + XCTAssertFalse(isFileRemoved) + + // -- Act -- + try sut.removeItemWithSentryTracing(atPath: fixture.filePathToDelete) + + // -- Assert -- + isFileRemoved = !FileManager.default.fileExists(atPath: fixture.filePathToDelete) + XCTAssertTrue(isFileRemoved) + + XCTAssertEqual(parentTransaction.children.count, 0) + } + + func testRemoveItemAtPathWithSentryTracing_fileIsIgnored_shouldNotTraceManually() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // Check pre-condition + var isFileRemoved = !FileManager.default.fileExists(atPath: fixture.ignoredFileToDeletePath) + XCTAssertFalse(isFileRemoved) + + // -- Act -- + try sut.removeItemWithSentryTracing(atPath: fixture.ignoredFileToDeletePath) + + // -- Assert -- + isFileRemoved = !FileManager.default.fileExists(atPath: fixture.ignoredFileToDeletePath) + XCTAssertTrue(isFileRemoved) + + XCTAssertEqual(parentTransaction.children.count, 0) + } + + // MARK: - FileManager.copyItemWithSentryTracing(at:to:) + + func testCopyItemAtUrlWithSentryTracing_shouldTraceManually() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // Check pre-conditions + var isSrcFileExisting = FileManager.default.fileExists(atPath: fixture.fileSrcUrl.path) + XCTAssertTrue(isSrcFileExisting) + var isDestFileExisting = FileManager.default.fileExists(atPath: fixture.fileDestUrl.path) + XCTAssertFalse(isDestFileExisting) + + let srcData = try Data(contentsOf: fixture.fileSrcUrl) + XCTAssertEqual(srcData, fixture.data) + + // -- Act -- + try sut.copyItemWithSentryTracing(at: fixture.fileSrcUrl, to: fixture.fileDestUrl) + + // -- Assert -- + isSrcFileExisting = FileManager.default.fileExists(atPath: fixture.fileSrcUrl.path) + XCTAssertTrue(isSrcFileExisting) + isDestFileExisting = FileManager.default.fileExists(atPath: fixture.fileDestUrl.path) + XCTAssertTrue(isDestFileExisting) + + let destData = try Data(contentsOf: fixture.fileDestUrl) + XCTAssertEqual(destData, fixture.data) + + XCTAssertEqual(parentTransaction.children.count, 1) + let span = try XCTUnwrap(parentTransaction.children.first) + XCTAssertEqual(span.origin, SentryTraceOrigin.manualFileData) + XCTAssertEqual(span.operation, SentrySpanOperation.fileCopy) + XCTAssertEqual(span.data["file.path"] as? String, fixture.fileSrcUrl.path) + } + + func testCopyItemAtUrlWithSentryTracing_throwsError_shouldTraceManuallyWithErrorRethrow() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // -- Act & Assert -- + XCTAssertThrowsError(try sut.copyItemWithSentryTracing(at: fixture.invalidSrcUrl, to: fixture.invalidDestUrl)) + + XCTAssertEqual(parentTransaction.children.count, 1) + let span = try XCTUnwrap(parentTransaction.children.first) + XCTAssertEqual(span.origin, SentryTraceOrigin.manualFileData) + XCTAssertEqual(span.operation, SentrySpanOperation.fileCopy) + XCTAssertEqual(span.data["file.path"] as? String, fixture.invalidSrcUrl.path) + } + + func testCopyItemAtUrlWithSentryTracing_trackerIsNotEnabled_shouldNotTraceManually() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name, isEnabled: false) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // Check pre-conditions + var isSrcFileExisting = FileManager.default.fileExists(atPath: fixture.fileSrcUrl.path) + XCTAssertTrue(isSrcFileExisting) + var isDestFileExisting = FileManager.default.fileExists(atPath: fixture.fileDestUrl.path) + XCTAssertFalse(isDestFileExisting) + + let srcData = try Data(contentsOf: fixture.fileSrcUrl) + XCTAssertEqual(srcData, fixture.data) + + // -- Act -- + try sut.copyItemWithSentryTracing(at: fixture.fileSrcUrl, to: fixture.fileDestUrl) + + // -- Assert -- + isSrcFileExisting = FileManager.default.fileExists(atPath: fixture.fileSrcUrl.path) + XCTAssertTrue(isSrcFileExisting) + isDestFileExisting = FileManager.default.fileExists(atPath: fixture.fileDestUrl.path) + XCTAssertTrue(isDestFileExisting) + + let destData = try Data(contentsOf: fixture.fileDestUrl) + XCTAssertEqual(destData, srcData) + + XCTAssertEqual(parentTransaction.children.count, 0) + } + + func testCopyItemAtUrlWithSentryTracing_fileIsIgnored_shouldNotTraceManually() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // Check pre-conditions + var isSrcFileExisting = FileManager.default.fileExists(atPath: fixture.ignoredSrcFileUrl.path) + XCTAssertTrue(isSrcFileExisting) + var isDestFileExisting = FileManager.default.fileExists(atPath: fixture.fileDestUrl.path) + XCTAssertFalse(isDestFileExisting) + + let srcData = try Data(contentsOf: fixture.fileSrcUrl) + XCTAssertEqual(srcData, fixture.data) + + // -- Act -- + try sut.copyItemWithSentryTracing(at: fixture.ignoredSrcFileUrl, to: fixture.fileDestUrl) + + // -- Assert -- + isSrcFileExisting = FileManager.default.fileExists(atPath: fixture.ignoredSrcFileUrl.path) + XCTAssertTrue(isSrcFileExisting) + isDestFileExisting = FileManager.default.fileExists(atPath: fixture.fileDestUrl.path) + XCTAssertTrue(isDestFileExisting) + + let destData = try Data(contentsOf: fixture.fileDestUrl) + XCTAssertEqual(destData, srcData) + + XCTAssertEqual(parentTransaction.children.count, 0) + } + + func testCopyItemAtUrlWithSentryTracing_nonFileUrl_shouldNotTraceManually() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // -- Act -- + XCTAssertThrowsError(try sut.copyItemWithSentryTracing(at: fixture.nonFileUrl, to: fixture.fileDestUrl)) + + // -- Assert -- + XCTAssertEqual(parentTransaction.children.count, 0) + } + + // MARK: - FileManager.copyItemWithSentryTracing(atPath:toPath:) + + func testCopyItemAtPathWithSentryTracing_shouldTraceManually() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // Check pre-conditions + var isSrcFileExisting = FileManager.default.fileExists(atPath: fixture.fileSrcPath) + XCTAssertTrue(isSrcFileExisting) + var isDestFileExisting = FileManager.default.fileExists(atPath: fixture.fileDestPath) + XCTAssertFalse(isDestFileExisting) + + let srcData = try Data(contentsOf: fixture.fileSrcUrl) + XCTAssertEqual(srcData, fixture.data) + + // -- Act -- + try sut.copyItemWithSentryTracing(at: fixture.fileSrcPath, to: fixture.fileDestPath) + + // -- Assert -- + // Smoke test to ensure the file is written + isSrcFileExisting = FileManager.default.fileExists(atPath: fixture.fileSrcPath) + XCTAssertTrue(isSrcFileExisting) + isDestFileExisting = FileManager.default.fileExists(atPath: fixture.fileDestPath) + XCTAssertTrue(isDestFileExisting) + + let writtenData = try Data(contentsOf: fixture.fileDestUrl) + XCTAssertEqual(writtenData, srcData) + + XCTAssertEqual(parentTransaction.children.count, 1) + let span = try XCTUnwrap(parentTransaction.children.first) + XCTAssertEqual(span.origin, SentryTraceOrigin.manualFileData) + XCTAssertEqual(span.operation, SentrySpanOperation.fileCopy) + XCTAssertEqual(span.data["file.path"] as? String, fixture.fileSrcPath) + } + + func testCopyItemAtPathWithSentryTracing_throwsError_shouldTraceManuallyWithErrorRethrow() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // -- Act & Assert -- + XCTAssertThrowsError(try sut.copyItemWithSentryTracing(at: fixture.invalidSrcPath, to: fixture.invalidDestPath)) + + XCTAssertEqual(parentTransaction.children.count, 1) + let span = try XCTUnwrap(parentTransaction.children.first) + XCTAssertEqual(span.origin, SentryTraceOrigin.manualFileData) + XCTAssertEqual(span.operation, SentrySpanOperation.fileCopy) + XCTAssertEqual(span.data["file.path"] as? String, fixture.invalidSrcPath) + } + + func testCopyItemAtPathWithSentryTracing_trackerIsNotEnabled_shouldNotTraceManually() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name, isEnabled: false) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // Check pre-conditions + var isSrcFileExisting = FileManager.default.fileExists(atPath: fixture.fileSrcPath) + XCTAssertTrue(isSrcFileExisting) + var isDestFileExisting = FileManager.default.fileExists(atPath: fixture.fileDestPath) + XCTAssertFalse(isDestFileExisting) + + let srcData = try Data(contentsOf: fixture.fileSrcUrl) + XCTAssertEqual(srcData, fixture.data) + + // -- Act -- + try sut.copyItemWithSentryTracing(at: fixture.fileSrcPath, to: fixture.fileDestPath) + + // -- Assert -- + isSrcFileExisting = FileManager.default.fileExists(atPath: fixture.fileSrcPath) + XCTAssertTrue(isSrcFileExisting) + isDestFileExisting = FileManager.default.fileExists(atPath: fixture.fileDestPath) + XCTAssertTrue(isDestFileExisting) + + let destData = try Data(contentsOf: fixture.fileDestUrl) + XCTAssertEqual(destData, srcData) + + XCTAssertEqual(parentTransaction.children.count, 0) + } + + func testCopyItemAtPathWithSentryTracing_fileIsIgnored_shouldNotTraceManually() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // Check pre-conditions + var isSrcFileExisting = FileManager.default.fileExists(atPath: fixture.ignoredSrcFilePath) + XCTAssertTrue(isSrcFileExisting) + var isDestFileExisting = FileManager.default.fileExists(atPath: fixture.fileDestPath) + XCTAssertFalse(isDestFileExisting) + + let srcData = try Data(contentsOf: fixture.fileSrcUrl) + XCTAssertEqual(srcData, fixture.data) + + // -- Act -- + try sut.copyItemWithSentryTracing(at: fixture.ignoredSrcFilePath, to: fixture.fileDestPath) + + // -- Assert -- + isSrcFileExisting = FileManager.default.fileExists(atPath: fixture.ignoredSrcFilePath) + XCTAssertTrue(isSrcFileExisting) + isDestFileExisting = FileManager.default.fileExists(atPath: fixture.fileDestPath) + XCTAssertTrue(isDestFileExisting) + + let destData = try Data(contentsOf: fixture.fileDestUrl) + XCTAssertEqual(destData, srcData) + + XCTAssertEqual(parentTransaction.children.count, 0) + } + + // MARK: - FileManager.moveItemWithSentryTracing(at:to:) + + func testMoveItemAtUrlWithSentryTracing_shouldTraceManually() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // Check pre-conditions + var isSrcFileExisting = FileManager.default.fileExists(atPath: fixture.fileSrcUrl.path) + XCTAssertTrue(isSrcFileExisting) + var isDestFileExisting = FileManager.default.fileExists(atPath: fixture.fileDestUrl.path) + XCTAssertFalse(isDestFileExisting) + + let srcData = try Data(contentsOf: fixture.fileSrcUrl) + XCTAssertEqual(srcData, fixture.data) + + // -- Act -- + try sut.moveItemWithSentryTracing(at: fixture.fileSrcUrl, to: fixture.fileDestUrl) + + // -- Assert -- + isSrcFileExisting = FileManager.default.fileExists(atPath: fixture.fileSrcUrl.path) + XCTAssertFalse(isSrcFileExisting) + isDestFileExisting = FileManager.default.fileExists(atPath: fixture.fileDestUrl.path) + XCTAssertTrue(isDestFileExisting) + + let destData = try Data(contentsOf: fixture.fileDestUrl) + XCTAssertEqual(destData, srcData) + + XCTAssertEqual(parentTransaction.children.count, 1) + let span = try XCTUnwrap(parentTransaction.children.first) + XCTAssertEqual(span.origin, SentryTraceOrigin.manualFileData) + XCTAssertEqual(span.operation, SentrySpanOperation.fileRename) + XCTAssertEqual(span.data["file.path"] as? String, fixture.fileSrcUrl.path) + } + + func testMoveItemAtUrlWithSentryTracing_throwsError_shouldTraceManually() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // -- Act & Assert -- + XCTAssertThrowsError(try sut.moveItemWithSentryTracing(at: fixture.invalidSrcUrl, to: fixture.invalidDestUrl)) + + XCTAssertEqual(parentTransaction.children.count, 1) + let span = try XCTUnwrap(parentTransaction.children.first) + XCTAssertEqual(span.origin, SentryTraceOrigin.manualFileData) + XCTAssertEqual(span.operation, SentrySpanOperation.fileRename) + XCTAssertEqual(span.data["file.path"] as? String, fixture.invalidSrcUrl.path) + } + + func testMoveItemAtUrlWithSentryTracing_trackerIsNotEnabled_shouldNotTraceManually() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name, isEnabled: false) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // Check pre-conditions + var isSrcFileExisting = FileManager.default.fileExists(atPath: fixture.fileSrcUrl.path) + XCTAssertTrue(isSrcFileExisting) + var isDestFileExisting = FileManager.default.fileExists(atPath: fixture.fileDestUrl.path) + XCTAssertFalse(isDestFileExisting) + + let srcData = try Data(contentsOf: fixture.fileSrcUrl) + XCTAssertEqual(srcData, fixture.data) + + // -- Act -- + try sut.moveItemWithSentryTracing(at: fixture.fileSrcUrl, to: fixture.fileDestUrl) + + // -- Assert -- + isSrcFileExisting = FileManager.default.fileExists(atPath: fixture.fileSrcUrl.path) + XCTAssertFalse(isSrcFileExisting) + isDestFileExisting = FileManager.default.fileExists(atPath: fixture.fileDestUrl.path) + XCTAssertTrue(isDestFileExisting) + + let destData = try Data(contentsOf: fixture.fileDestUrl) + XCTAssertEqual(destData, srcData) + + XCTAssertEqual(parentTransaction.children.count, 0) + } + + func testMoveItemAtUrlWithSentryTracing_fileIsIgnored_shouldNotTraceManually() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // Check pre-conditions + var isSrcFileExisting = FileManager.default.fileExists(atPath: fixture.ignoredSrcFileUrl.path) + XCTAssertTrue(isSrcFileExisting) + var isDestFileExisting = FileManager.default.fileExists(atPath: fixture.fileDestUrl.path) + XCTAssertFalse(isDestFileExisting) + + let srcData = try Data(contentsOf: fixture.fileSrcUrl) + XCTAssertEqual(srcData, fixture.data) + + // -- Act -- + try sut.moveItemWithSentryTracing(at: fixture.ignoredSrcFileUrl, to: fixture.fileDestUrl) + + // -- Assert -- + isSrcFileExisting = FileManager.default.fileExists(atPath: fixture.ignoredSrcFileUrl.path) + XCTAssertFalse(isSrcFileExisting) + isDestFileExisting = FileManager.default.fileExists(atPath: fixture.fileDestUrl.path) + XCTAssertTrue(isDestFileExisting) + + let destData = try Data(contentsOf: fixture.fileDestUrl) + XCTAssertEqual(destData, srcData) + + XCTAssertEqual(parentTransaction.children.count, 0) + } + + func testMoveItemAtUrlWithSentryTracing_nonFileUrl_shouldNotTraceManually() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // -- Act -- + XCTAssertThrowsError(try sut.moveItemWithSentryTracing(at: fixture.nonFileUrl, to: fixture.fileDestUrl)) + + // -- Assert -- + XCTAssertEqual(parentTransaction.children.count, 0) + } + + // MARK: - FileManager.moveItemWithSentryTracing(atPath:toPath:) + + func testMoveItemAtPathWithSentryTracing_shouldTraceManually() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // Check pre-conditions + var isSrcFileExisting = FileManager.default.fileExists(atPath: fixture.fileSrcPath) + XCTAssertTrue(isSrcFileExisting) + var isDestFileExisting = FileManager.default.fileExists(atPath: fixture.fileDestPath) + XCTAssertFalse(isDestFileExisting) + + let srcData = try Data(contentsOf: fixture.fileSrcUrl) + XCTAssertEqual(srcData, fixture.data) + + // -- Act -- + try sut.moveItemWithSentryTracing(at: fixture.fileSrcPath, to: fixture.fileDestPath) + + // -- Assert -- + isSrcFileExisting = FileManager.default.fileExists(atPath: fixture.fileSrcPath) + XCTAssertFalse(isSrcFileExisting) + isDestFileExisting = FileManager.default.fileExists(atPath: fixture.fileDestPath) + XCTAssertTrue(isDestFileExisting) + + let destData = try Data(contentsOf: fixture.fileDestUrl) + XCTAssertEqual(destData, srcData) + + XCTAssertEqual(parentTransaction.children.count, 1) + let span = try XCTUnwrap(parentTransaction.children.first) + XCTAssertEqual(span.origin, SentryTraceOrigin.manualFileData) + XCTAssertEqual(span.operation, SentrySpanOperation.fileRename) + XCTAssertEqual(span.data["file.path"] as? String, fixture.fileSrcPath) + } + + func testMoveItemAtPathWithSentryTracing_throwsError_shouldTraceManually() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // -- Act & Assert -- + XCTAssertThrowsError(try sut.moveItemWithSentryTracing(at: fixture.invalidSrcPath, to: fixture.invalidDestPath)) + + XCTAssertEqual(parentTransaction.children.count, 1) + let span = try XCTUnwrap(parentTransaction.children.first) + XCTAssertEqual(span.origin, SentryTraceOrigin.manualFileData) + XCTAssertEqual(span.operation, SentrySpanOperation.fileRename) + XCTAssertEqual(span.data["file.path"] as? String, fixture.invalidSrcPath) + } + + func testMoveItemAtPathWithSentryTracing_trackerIsNotEnabled_shouldNotTraceManually() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name, isEnabled: false) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // Check pre-conditions + var isSrcFileExisting = FileManager.default.fileExists(atPath: fixture.fileSrcPath) + XCTAssertTrue(isSrcFileExisting) + var isDestFileExisting = FileManager.default.fileExists(atPath: fixture.fileDestPath) + XCTAssertFalse(isDestFileExisting) + + let srcData = try Data(contentsOf: fixture.fileSrcUrl) + XCTAssertEqual(srcData, fixture.data) + + // -- Act -- + try sut.moveItemWithSentryTracing(at: fixture.fileSrcPath, to: fixture.fileDestPath) + + // -- Assert -- + isSrcFileExisting = FileManager.default.fileExists(atPath: fixture.fileSrcPath) + XCTAssertFalse(isSrcFileExisting) + isDestFileExisting = FileManager.default.fileExists(atPath: fixture.fileDestPath) + XCTAssertTrue(isDestFileExisting) + + let destData = try Data(contentsOf: fixture.fileDestUrl) + XCTAssertEqual(destData, srcData) + + XCTAssertEqual(parentTransaction.children.count, 0) + } + + func testMoveItemAtPathWithSentryTracing_fileIsIgnored_shouldNotTraceManually() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // Check pre-conditions + var isSrcFileExisting = FileManager.default.fileExists(atPath: fixture.ignoredSrcFilePath) + XCTAssertTrue(isSrcFileExisting) + var isDestFileExisting = FileManager.default.fileExists(atPath: fixture.fileDestPath) + XCTAssertFalse(isDestFileExisting) + + let srcData = try Data(contentsOf: fixture.fileSrcUrl) + XCTAssertEqual(srcData, fixture.data) + + // -- Act -- + try sut.moveItemWithSentryTracing(at: fixture.ignoredSrcFilePath, to: fixture.fileDestPath) + + // -- Assert -- + isSrcFileExisting = FileManager.default.fileExists(atPath: fixture.ignoredSrcFilePath) + XCTAssertFalse(isSrcFileExisting) + isDestFileExisting = FileManager.default.fileExists(atPath: fixture.fileDestPath) + XCTAssertTrue(isDestFileExisting) + + let destData = try Data(contentsOf: fixture.fileDestUrl) + XCTAssertEqual(destData, srcData) + + XCTAssertEqual(parentTransaction.children.count, 0) + } +}