diff --git a/FirebaseCore/Internal/Sources/HeartbeatLogging/HeartbeatStorage.swift b/FirebaseCore/Internal/Sources/HeartbeatLogging/HeartbeatStorage.swift index ff428077613..991e6e15a15 100644 --- a/FirebaseCore/Internal/Sources/HeartbeatLogging/HeartbeatStorage.swift +++ b/FirebaseCore/Internal/Sources/HeartbeatLogging/HeartbeatStorage.swift @@ -17,19 +17,20 @@ import Foundation /// A type that can perform atomic operations using block-based transformations. protocol HeartbeatStorageProtocol { func readAndWriteSync(using transform: (HeartbeatsBundle?) -> HeartbeatsBundle?) - func readAndWriteAsync(using transform: @escaping (HeartbeatsBundle?) -> HeartbeatsBundle?) + func readAndWriteAsync(using transform: @escaping @Sendable (HeartbeatsBundle?) + -> HeartbeatsBundle?) func getAndSet(using transform: (HeartbeatsBundle?) -> HeartbeatsBundle?) throws -> HeartbeatsBundle? - func getAndSetAsync(using transform: @escaping (HeartbeatsBundle?) -> HeartbeatsBundle?, - completion: @escaping (Result) -> Void) + func getAndSetAsync(using transform: @escaping @Sendable (HeartbeatsBundle?) -> HeartbeatsBundle?, + completion: @escaping @Sendable (Result) -> Void) } /// Thread-safe storage object designed for transforming heartbeat data that is persisted to disk. -final class HeartbeatStorage: HeartbeatStorageProtocol { +final class HeartbeatStorage: Sendable, HeartbeatStorageProtocol { /// The identifier used to differentiate instances. private let id: String /// The underlying storage container to read from and write to. - private let storage: Storage + private let storage: any Storage /// The encoder used for encoding heartbeat data. private let encoder: JSONEncoder = .init() /// The decoder used for decoding heartbeat data. @@ -107,7 +108,8 @@ final class HeartbeatStorage: HeartbeatStorageProtocol { /// Asynchronously reads from and writes to storage using the given transform block. /// - Parameter transform: A block to transform the currently stored heartbeats bundle to a new /// heartbeats bundle value. - func readAndWriteAsync(using transform: @escaping (HeartbeatsBundle?) -> HeartbeatsBundle?) { + func readAndWriteAsync(using transform: @escaping @Sendable (HeartbeatsBundle?) + -> HeartbeatsBundle?) { queue.async { [self] in let oldHeartbeatsBundle = try? load(from: storage) let newHeartbeatsBundle = transform(oldHeartbeatsBundle) @@ -143,8 +145,8 @@ final class HeartbeatStorage: HeartbeatStorageProtocol { /// - completion: An escaping block used to process the heartbeat data that /// was stored (before the `transform` was applied); otherwise, the error /// that occurred. - func getAndSetAsync(using transform: @escaping (HeartbeatsBundle?) -> HeartbeatsBundle?, - completion: @escaping (Result) -> Void) { + func getAndSetAsync(using transform: @escaping @Sendable (HeartbeatsBundle?) -> HeartbeatsBundle?, + completion: @escaping @Sendable (Result) -> Void) { queue.async { do { let oldHeartbeatsBundle = try? self.load(from: self.storage) diff --git a/FirebaseCore/Internal/Sources/HeartbeatLogging/Storage.swift b/FirebaseCore/Internal/Sources/HeartbeatLogging/Storage.swift index a4cac33e27a..69bf613b690 100644 --- a/FirebaseCore/Internal/Sources/HeartbeatLogging/Storage.swift +++ b/FirebaseCore/Internal/Sources/HeartbeatLogging/Storage.swift @@ -15,7 +15,7 @@ import Foundation /// A type that reads from and writes to an underlying storage container. -protocol Storage { +protocol Storage: Sendable { /// Reads and returns the data stored by this storage type. /// - Returns: The data read from storage. /// - Throws: An error if the read failed. @@ -38,16 +38,12 @@ enum StorageError: Error { final class FileStorage: Storage { /// A file system URL to the underlying file resource. private let url: URL - /// The file manager used to perform file system operations. - private let fileManager: FileManager /// Designated initializer. /// - Parameters: /// - url: A file system URL for the underlying file resource. - /// - fileManager: A file manager. Defaults to `default` manager. - init(url: URL, fileManager: FileManager = .default) { + init(url: URL) { self.url = url - self.fileManager = fileManager } /// Reads and returns the data from this object's associated file resource. @@ -90,7 +86,7 @@ final class FileStorage: Storage { /// - Parameter url: The URL to create directories in. private func createDirectories(in url: URL) throws { do { - try fileManager.createDirectory( + try FileManager.default.createDirectory( at: url, withIntermediateDirectories: true ) @@ -104,17 +100,26 @@ final class FileStorage: Storage { /// A object that provides API for reading and writing to a user defaults resource. final class UserDefaultsStorage: Storage { - /// The underlying defaults container. - private let defaults: UserDefaults + /// The suite name for the underlying defaults container. + private let suiteName: String + /// The key mapping to the object's associated resource in `defaults`. private let key: String + /// The underlying defaults container. + private var defaults: UserDefaults { + // It's safe to force unwrap the below defaults instance because the + // initializer only returns `nil` when the bundle id or `globalDomain` + // is passed in as the `suiteName`. + UserDefaults(suiteName: suiteName)! + } + /// Designated initializer. /// - Parameters: - /// - defaults: The defaults container. + /// - suiteName: The suite name for the defaults container. /// - key: The key mapping to the value stored in the defaults container. - init(defaults: UserDefaults, key: String) { - self.defaults = defaults + init(suiteName: String, key: String) { + self.suiteName = suiteName self.key = key } diff --git a/FirebaseCore/Internal/Sources/HeartbeatLogging/StorageFactory.swift b/FirebaseCore/Internal/Sources/HeartbeatLogging/StorageFactory.swift index 6552a318158..d6d97cf78ba 100644 --- a/FirebaseCore/Internal/Sources/HeartbeatLogging/StorageFactory.swift +++ b/FirebaseCore/Internal/Sources/HeartbeatLogging/StorageFactory.swift @@ -56,11 +56,7 @@ extension FileManager { extension UserDefaultsStorage: StorageFactory { static func makeStorage(id: String) -> Storage { let suiteName = Constants.heartbeatUserDefaultsSuiteName - // It's safe to force unwrap the below defaults instance because the - // initializer only returns `nil` when the bundle id or `globalDomain` - // is passed in as the `suiteName`. - let defaults = UserDefaults(suiteName: suiteName)! let key = "heartbeats-\(id)" - return UserDefaultsStorage(defaults: defaults, key: key) + return UserDefaultsStorage(suiteName: suiteName, key: key) } } diff --git a/FirebaseCore/Internal/Tests/Unit/StorageTests.swift b/FirebaseCore/Internal/Tests/Unit/StorageTests.swift index 21b4fded8c1..adc47ba81d6 100644 --- a/FirebaseCore/Internal/Tests/Unit/StorageTests.swift +++ b/FirebaseCore/Internal/Tests/Unit/StorageTests.swift @@ -97,22 +97,23 @@ class FileStorageTests: XCTestCase { class UserDefaultsStorageTests: XCTestCase { var defaults: UserDefaults! - let suiteName = #file + let suiteName = "com.firebase.userdefaults.storageTests" override func setUpWithError() throws { - defaults = try XCTUnwrap(UserDefaultsFake(suiteName: suiteName)) + // Clear the user default suite before testing. + UserDefaults(suiteName: suiteName)?.removePersistentDomain(forName: suiteName) } func testRead_WhenDefaultDoesNotExist_ThrowsError() throws { // Given - let defaultsStorage = UserDefaultsStorage(defaults: defaults, key: #function) + let defaultsStorage = UserDefaultsStorage(suiteName: suiteName, key: #function) // Then XCTAssertThrowsError(try defaultsStorage.read()) } func testRead_WhenDefaultExists_ReturnsDefault() throws { // Given - let defaultsStorage = UserDefaultsStorage(defaults: defaults, key: #function) + let defaultsStorage = UserDefaultsStorage(suiteName: suiteName, key: #function) XCTAssertNoThrow(try defaultsStorage.write(Constants.testData)) // When let storedData = try defaultsStorage.read() @@ -122,7 +123,7 @@ class UserDefaultsStorageTests: XCTestCase { func testWriteData_WhenDefaultDoesNotExist_CreatesDefault() throws { // Given - let defaultsStorage = UserDefaultsStorage(defaults: defaults, key: #function) + let defaultsStorage = UserDefaultsStorage(suiteName: suiteName, key: #function) XCTAssertThrowsError(try defaultsStorage.read()) // When XCTAssertNoThrow(try defaultsStorage.write(Constants.testData)) @@ -133,7 +134,7 @@ class UserDefaultsStorageTests: XCTestCase { func testWriteData_WhenDefaultExists_ModifiesDefault() throws { // Given - let defaultsStorage = UserDefaultsStorage(defaults: defaults, key: #function) + let defaultsStorage = UserDefaultsStorage(suiteName: suiteName, key: #function) XCTAssertNoThrow(try defaultsStorage.write(Constants.testData)) // When let modifiedData = #function.data(using: .utf8) @@ -146,7 +147,7 @@ class UserDefaultsStorageTests: XCTestCase { func testWriteNil_WhenDefaultDoesNotExist_RemovesDefault() throws { // Given - let defaultsStorage = UserDefaultsStorage(defaults: defaults, key: #function) + let defaultsStorage = UserDefaultsStorage(suiteName: suiteName, key: #function) XCTAssertThrowsError(try defaultsStorage.read()) // When XCTAssertNoThrow(try defaultsStorage.write(nil)) @@ -156,7 +157,7 @@ class UserDefaultsStorageTests: XCTestCase { func testWriteNil_WhenDefaultExists_RemovesDefault() throws { // Given - let defaultsStorage = UserDefaultsStorage(defaults: defaults, key: #function) + let defaultsStorage = UserDefaultsStorage(suiteName: suiteName, key: #function) XCTAssertNoThrow(try defaultsStorage.write(Constants.testData)) // When XCTAssertNoThrow(try defaultsStorage.write(nil)) @@ -164,17 +165,3 @@ class UserDefaultsStorageTests: XCTestCase { XCTAssertThrowsError(try defaultsStorage.read()) } } - -// MARK: - Fakes - -private class UserDefaultsFake: UserDefaults { - private var defaults = [String: Any]() - - override func object(forKey defaultName: String) -> Any? { - defaults[defaultName] - } - - override func set(_ value: Any?, forKey defaultName: String) { - defaults[defaultName] = value - } -}