From e18d225dbdb958961b217472893f0127363fcc67 Mon Sep 17 00:00:00 2001 From: Anthony Miller Date: Fri, 27 Sep 2024 11:49:24 -0700 Subject: [PATCH] Add async setup to TestCacheProvider --- .../SQLiteTestCacheProvider.swift | 27 +- .../TestCacheProvider.swift | 46 +-- .../CacheDependentInterceptorTests.swift | 8 +- .../Cache/DeferOperationCacheReadTests.swift | 6 +- .../Cache/DeferOperationCacheWriteTests.swift | 6 +- Tests/ApolloTests/Cache/FetchQueryTests.swift | 8 +- .../Cache/LoadQueryFromStoreTests.swift | 8 +- .../Cache/ReadWriteFromStoreTests.swift | 6 +- .../Cache/SQLite/CachePersistenceTests.swift | 276 +++++++++--------- .../Cache/StoreConcurrencyTests.swift | 8 +- Tests/ApolloTests/Cache/WatchQueryTests.swift | 8 +- 11 files changed, 198 insertions(+), 209 deletions(-) diff --git a/Tests/ApolloInternalTestHelpers/SQLiteTestCacheProvider.swift b/Tests/ApolloInternalTestHelpers/SQLiteTestCacheProvider.swift index d82b214b9..db27acff1 100644 --- a/Tests/ApolloInternalTestHelpers/SQLiteTestCacheProvider.swift +++ b/Tests/ApolloInternalTestHelpers/SQLiteTestCacheProvider.swift @@ -2,22 +2,17 @@ import Foundation import Apollo import ApolloSQLite -public class SQLiteTestCacheProvider: TestCacheProvider { - /// Execute a test block rather than return a cache synchronously, since cache setup may be - /// asynchronous at some point. - public static func withCache(initialRecords: RecordSet? = nil, fileURL: URL? = nil, execute test: (any NormalizedCache) throws -> ()) throws { - let fileURL = fileURL ?? temporarySQLiteFileURL() - let cache = try! SQLiteNormalizedCache(fileURL: fileURL) - if let initialRecords = initialRecords { - _ = try cache.merge(records: initialRecords) - } - try test(cache) +public class SQLiteTestCacheProvider: TestCacheProvider { + public static func makeNormalizedCache() async -> TestDependency { + await makeNormalizedCache(fileURL: temporarySQLiteFileURL()) } - - public static func makeNormalizedCache(_ completionHandler: (Result, any Error>) -> ()) { - let fileURL = temporarySQLiteFileURL() + + public static func makeNormalizedCache(fileURL: URL) async -> TestDependency { let cache = try! SQLiteNormalizedCache(fileURL: fileURL) - completionHandler(.success((cache, nil))) + let tearDownHandler = { @Sendable in + try Self.deleteCache(at: fileURL) + } + return (cache, tearDownHandler) } public static func temporarySQLiteFileURL() -> URL { @@ -30,4 +25,8 @@ public class SQLiteTestCacheProvider: TestCacheProvider { return folder.appendingPathComponent("db.sqlite3") } + + static func deleteCache(at url: URL) throws { + try FileManager.default.removeItem(at: url) + } } diff --git a/Tests/ApolloInternalTestHelpers/TestCacheProvider.swift b/Tests/ApolloInternalTestHelpers/TestCacheProvider.swift index 1371a305e..09d99965d 100644 --- a/Tests/ApolloInternalTestHelpers/TestCacheProvider.swift +++ b/Tests/ApolloInternalTestHelpers/TestCacheProvider.swift @@ -1,17 +1,17 @@ import XCTest import Apollo -public typealias TearDownHandler = () throws -> () +public typealias TearDownHandler = @Sendable () throws -> () public typealias TestDependency = (Resource, TearDownHandler?) public protocol TestCacheProvider: AnyObject { - static func makeNormalizedCache(_ completionHandler: (Result, any Error>) -> ()) + static func makeNormalizedCache() async -> TestDependency } public class InMemoryTestCacheProvider: TestCacheProvider { - public static func makeNormalizedCache(_ completionHandler: (Result, any Error>) -> ()) { + public static func makeNormalizedCache() async -> TestDependency { let cache = InMemoryNormalizedCache() - completionHandler(.success((cache, nil))) + return (cache, nil) } } @@ -21,36 +21,20 @@ public protocol CacheDependentTesting { } extension CacheDependentTesting where Self: XCTestCase { - public func makeNormalizedCache() throws -> any NormalizedCache { - var result: Result = .failure(XCTestError(.timeoutWhileWaiting)) - - let expectation = XCTestExpectation(description: "Initialized normalized cache") - - cacheType.makeNormalizedCache() { [weak self] testDependencyResult in - guard let self = self else { return } - - result = testDependencyResult.map { testDependency in - let (cache, tearDownHandler) = testDependency - - if let tearDownHandler = tearDownHandler { - self.addTeardownBlock { - do { - try tearDownHandler() - } catch { - self.record(error) - } - } + public func makeNormalizedCache() async throws -> any NormalizedCache { + let (cache, tearDownHandler) = await cacheType.makeNormalizedCache() + + if let tearDownHandler = tearDownHandler { + self.addTeardownBlock { + do { + try tearDownHandler() + } catch { + self.record(error) } - - return cache } - - expectation.fulfill() } - - wait(for: [expectation], timeout: 1) - - return try result.get() + + return cache } public func mergeRecordsIntoCache(_ records: RecordSet) { diff --git a/Tests/ApolloTests/Cache/CacheDependentInterceptorTests.swift b/Tests/ApolloTests/Cache/CacheDependentInterceptorTests.swift index 91d464dbd..77a6d0ed5 100644 --- a/Tests/ApolloTests/Cache/CacheDependentInterceptorTests.swift +++ b/Tests/ApolloTests/Cache/CacheDependentInterceptorTests.swift @@ -11,10 +11,10 @@ class CacheDependentInterceptorTests: XCTestCase, CacheDependentTesting { var cache: (any NormalizedCache)! var store: ApolloStore! - override func setUpWithError() throws { - try super.setUpWithError() - - cache = try makeNormalizedCache() + override func setUp() async throws { + try await super.setUp() + + cache = try await makeNormalizedCache() store = ApolloStore(cache: cache) } diff --git a/Tests/ApolloTests/Cache/DeferOperationCacheReadTests.swift b/Tests/ApolloTests/Cache/DeferOperationCacheReadTests.swift index b650ea699..060693b7b 100644 --- a/Tests/ApolloTests/Cache/DeferOperationCacheReadTests.swift +++ b/Tests/ApolloTests/Cache/DeferOperationCacheReadTests.swift @@ -93,10 +93,10 @@ class DeferOperationCacheReadTests: XCTestCase, CacheDependentTesting { var server: MockGraphQLServer! var client: ApolloClient! - override func setUpWithError() throws { - try super.setUpWithError() + override func setUp() async throws { + try await super.setUp() - cache = try makeNormalizedCache() + cache = try await makeNormalizedCache() let store = ApolloStore(cache: cache) server = MockGraphQLServer() diff --git a/Tests/ApolloTests/Cache/DeferOperationCacheWriteTests.swift b/Tests/ApolloTests/Cache/DeferOperationCacheWriteTests.swift index bb1eabf01..0fec6dc92 100644 --- a/Tests/ApolloTests/Cache/DeferOperationCacheWriteTests.swift +++ b/Tests/ApolloTests/Cache/DeferOperationCacheWriteTests.swift @@ -89,10 +89,10 @@ class DeferOperationCacheWriteTests: XCTestCase, CacheDependentTesting, StoreLoa var cache: (any NormalizedCache)! var store: ApolloStore! - override func setUpWithError() throws { - try super.setUpWithError() + override func setUp() async throws { + try await super.setUp() - cache = try makeNormalizedCache() + cache = try await makeNormalizedCache() store = ApolloStore(cache: cache) } diff --git a/Tests/ApolloTests/Cache/FetchQueryTests.swift b/Tests/ApolloTests/Cache/FetchQueryTests.swift index 1f01ae83a..4a8184955 100644 --- a/Tests/ApolloTests/Cache/FetchQueryTests.swift +++ b/Tests/ApolloTests/Cache/FetchQueryTests.swift @@ -15,10 +15,10 @@ class FetchQueryTests: XCTestCase, CacheDependentTesting { var server: MockGraphQLServer! var client: ApolloClient! - override func setUpWithError() throws { - try super.setUpWithError() - - cache = try makeNormalizedCache() + override func setUp() async throws { + try await super.setUp() + + cache = try await makeNormalizedCache() let store = ApolloStore(cache: cache) server = MockGraphQLServer() diff --git a/Tests/ApolloTests/Cache/LoadQueryFromStoreTests.swift b/Tests/ApolloTests/Cache/LoadQueryFromStoreTests.swift index 2cde09a01..8af21cfb2 100644 --- a/Tests/ApolloTests/Cache/LoadQueryFromStoreTests.swift +++ b/Tests/ApolloTests/Cache/LoadQueryFromStoreTests.swift @@ -16,10 +16,10 @@ class LoadQueryFromStoreTests: XCTestCase, CacheDependentTesting, StoreLoading { var cache: (any NormalizedCache)! var store: ApolloStore! - override func setUpWithError() throws { - try super.setUpWithError() - - cache = try makeNormalizedCache() + override func setUp() async throws { + try await super.setUp() + + cache = try await makeNormalizedCache() store = ApolloStore(cache: cache) } diff --git a/Tests/ApolloTests/Cache/ReadWriteFromStoreTests.swift b/Tests/ApolloTests/Cache/ReadWriteFromStoreTests.swift index 413884347..fd9d68813 100644 --- a/Tests/ApolloTests/Cache/ReadWriteFromStoreTests.swift +++ b/Tests/ApolloTests/Cache/ReadWriteFromStoreTests.swift @@ -15,10 +15,10 @@ class ReadWriteFromStoreTests: XCTestCase, CacheDependentTesting, StoreLoading { var cache: (any NormalizedCache)! var store: ApolloStore! - override func setUpWithError() throws { - try super.setUpWithError() + override func setUp() async throws { + try await super.setUp() - cache = try makeNormalizedCache() + cache = try await makeNormalizedCache() store = ApolloStore(cache: cache) } diff --git a/Tests/ApolloTests/Cache/SQLite/CachePersistenceTests.swift b/Tests/ApolloTests/Cache/SQLite/CachePersistenceTests.swift index 0300f8bc5..548132770 100644 --- a/Tests/ApolloTests/Cache/SQLite/CachePersistenceTests.swift +++ b/Tests/ApolloTests/Cache/SQLite/CachePersistenceTests.swift @@ -8,7 +8,7 @@ import StarWarsAPI class CachePersistenceTests: XCTestCase { - func testFetchAndPersist() throws { + func testFetchAndPersist() async throws { // given class GivenSelectionSet: MockSelectionSet { override class var __selections: [Selection] { [ @@ -26,64 +26,66 @@ class CachePersistenceTests: XCTestCase { let query = MockQuery() let sqliteFileURL = SQLiteTestCacheProvider.temporarySQLiteFileURL() - try SQLiteTestCacheProvider.withCache(fileURL: sqliteFileURL) { (cache) in - let store = ApolloStore(cache: cache) + let (cache, tearDown) = await SQLiteTestCacheProvider.makeNormalizedCache(fileURL: sqliteFileURL) + if let tearDown { self.addTeardownBlock(tearDown) } + let store = ApolloStore(cache: cache) - let server = MockGraphQLServer() - let networkTransport = MockNetworkTransport(server: server, store: store) + let server = MockGraphQLServer() + let networkTransport = MockNetworkTransport(server: server, store: store) - let client = ApolloClient(networkTransport: networkTransport, store: store) + let client = ApolloClient(networkTransport: networkTransport, store: store) - _ = server.expect(MockQuery.self) { request in - [ - "data": [ - "hero": [ - "name": "Luke Skywalker", - "__typename": "Human" - ] + _ = server.expect(MockQuery.self) { request in + [ + "data": [ + "hero": [ + "name": "Luke Skywalker", + "__typename": "Human" ] ] - } + ] + } - let networkExpectation = self.expectation(description: "Fetching query from network") - let newCacheExpectation = self.expectation(description: "Fetch query from new cache") + let networkExpectation = self.expectation(description: "Fetching query from network") + let newCacheExpectation = self.expectation(description: "Fetch query from new cache") - client.fetch(query: query, cachePolicy: .fetchIgnoringCacheData) { outerResult in - defer { networkExpectation.fulfill() } + client.fetch(query: query, cachePolicy: .fetchIgnoringCacheData) { outerResult in + defer { networkExpectation.fulfill() } - switch outerResult { - case .failure(let error): - XCTFail("Unexpected error: \(error)") - return - case .success(let graphQLResult): - XCTAssertEqual(graphQLResult.data?.hero?.name, "Luke Skywalker") - - // Do another fetch from cache to ensure that data is cached before creating new cache - client.fetch(query: query, cachePolicy: .returnCacheDataDontFetch) { innerResult in - try! SQLiteTestCacheProvider.withCache(fileURL: sqliteFileURL) { cache in - let newStore = ApolloStore(cache: cache) - let newClient = ApolloClient(networkTransport: networkTransport, store: newStore) - - newClient.fetch(query: query, cachePolicy: .returnCacheDataDontFetch) { newClientResult in - defer { newCacheExpectation.fulfill() } - switch newClientResult { - case .success(let newClientGraphQLResult): - XCTAssertEqual(newClientGraphQLResult.data?.hero?.name, "Luke Skywalker") - case .failure(let error): - XCTFail("Unexpected error with new client: \(error)") - } - _ = newClient // Workaround for a bug - ensure that newClient is retained until this block is run + switch outerResult { + case .failure(let error): + XCTFail("Unexpected error: \(error)") + return + case .success(let graphQLResult): + XCTAssertEqual(graphQLResult.data?.hero?.name, "Luke Skywalker") + + // Do another fetch from cache to ensure that data is cached before creating new cache + client.fetch(query: query, cachePolicy: .returnCacheDataDontFetch) { innerResult in + Task { + let (cache, _) = await SQLiteTestCacheProvider.makeNormalizedCache(fileURL: sqliteFileURL) + let newStore = ApolloStore(cache: cache) + let newClient = ApolloClient(networkTransport: networkTransport, store: newStore) + + newClient.fetch(query: query, cachePolicy: .returnCacheDataDontFetch) { newClientResult in + defer { newCacheExpectation.fulfill() } + switch newClientResult { + case .success(let newClientGraphQLResult): + XCTAssertEqual(newClientGraphQLResult.data?.hero?.name, "Luke Skywalker") + case .failure(let error): + XCTFail("Unexpected error with new client: \(error)") } + _ = newClient // Workaround for a bug - ensure that newClient is retained until this block is run } } + } } - - self.waitForExpectations(timeout: 2, handler: nil) } + + await fulfillment(of: [networkExpectation, newCacheExpectation], timeout: 2) } - func testFetchAndPersistWithPeriodArguments() throws { + func testFetchAndPersistWithPeriodArguments() async throws { // given class GivenSelectionSet: MockSelectionSet { override class var __selections: [Selection] { [ @@ -102,61 +104,62 @@ class CachePersistenceTests: XCTestCase { let sqliteFileURL = SQLiteTestCacheProvider.temporarySQLiteFileURL() - try SQLiteTestCacheProvider.withCache(fileURL: sqliteFileURL) { (cache) in - let store = ApolloStore(cache: cache) + let (cache, tearDown) = await SQLiteTestCacheProvider.makeNormalizedCache(fileURL: sqliteFileURL) + if let tearDown { self.addTeardownBlock(tearDown) } + let store = ApolloStore(cache: cache) - let server = MockGraphQLServer() - let networkTransport = MockNetworkTransport(server: server, store: store) + let server = MockGraphQLServer() + let networkTransport = MockNetworkTransport(server: server, store: store) - let client = ApolloClient(networkTransport: networkTransport, store: store) + let client = ApolloClient(networkTransport: networkTransport, store: store) - _ = server.expect(MockQuery.self) { request in - [ - "data": [ - "hero": [ - "name": "Luke Skywalker", - "__typename": "Human" - ] + _ = server.expect(MockQuery.self) { request in + [ + "data": [ + "hero": [ + "name": "Luke Skywalker", + "__typename": "Human" ] ] - } + ] + } - let networkExpectation = self.expectation(description: "Fetching query from network") - let newCacheExpectation = self.expectation(description: "Fetch query from new cache") + let networkExpectation = self.expectation(description: "Fetching query from network") + let newCacheExpectation = self.expectation(description: "Fetch query from new cache") - client.fetch(query: query, cachePolicy: .fetchIgnoringCacheData) { outerResult in - defer { networkExpectation.fulfill() } + client.fetch(query: query, cachePolicy: .fetchIgnoringCacheData) { outerResult in + defer { networkExpectation.fulfill() } - switch outerResult { - case .failure(let error): - XCTFail("Unexpected error: \(error)") - return - case .success(let graphQLResult): - XCTAssertEqual(graphQLResult.data?.hero?.name, "Luke Skywalker") - - // Do another fetch from cache to ensure that data is cached before creating new cache - client.fetch(query: query, cachePolicy: .returnCacheDataDontFetch) { innerResult in - try! SQLiteTestCacheProvider.withCache(fileURL: sqliteFileURL) { cache in - let newStore = ApolloStore(cache: cache) - let newClient = ApolloClient(networkTransport: networkTransport, store: newStore) - - newClient.fetch(query: query, cachePolicy: .returnCacheDataDontFetch) { newClientResult in - defer { newCacheExpectation.fulfill() } - switch newClientResult { - case .success(let newClientGraphQLResult): - XCTAssertEqual(newClientGraphQLResult.data?.hero?.name, "Luke Skywalker") - case .failure(let error): - XCTFail("Unexpected error with new client: \(error)") - } - _ = newClient // Workaround for a bug - ensure that newClient is retained until this block is run + switch outerResult { + case .failure(let error): + XCTFail("Unexpected error: \(error)") + return + case .success(let graphQLResult): + XCTAssertEqual(graphQLResult.data?.hero?.name, "Luke Skywalker") + + // Do another fetch from cache to ensure that data is cached before creating new cache + client.fetch(query: query, cachePolicy: .returnCacheDataDontFetch) { innerResult in + Task { + let (cache, _) = await SQLiteTestCacheProvider.makeNormalizedCache(fileURL: sqliteFileURL) + let newStore = ApolloStore(cache: cache) + let newClient = ApolloClient(networkTransport: networkTransport, store: newStore) + + newClient.fetch(query: query, cachePolicy: .returnCacheDataDontFetch) { newClientResult in + defer { newCacheExpectation.fulfill() } + switch newClientResult { + case .success(let newClientGraphQLResult): + XCTAssertEqual(newClientGraphQLResult.data?.hero?.name, "Luke Skywalker") + case .failure(let error): + XCTFail("Unexpected error with new client: \(error)") } + _ = newClient // Workaround for a bug - ensure that newClient is retained until this block is run } } } } - - self.waitForExpectations(timeout: 2, handler: nil) } + + await fulfillment(of: [networkExpectation, newCacheExpectation], timeout: 2) } func testPassInConnectionDoesNotThrow() { @@ -169,7 +172,7 @@ class CachePersistenceTests: XCTestCase { } } - func testClearCache() throws { + func testClearCache() async throws { // given class GivenSelectionSet: MockSelectionSet { override class var __selections: [Selection] { [ @@ -187,73 +190,76 @@ class CachePersistenceTests: XCTestCase { let query = MockQuery() let sqliteFileURL = SQLiteTestCacheProvider.temporarySQLiteFileURL() - try SQLiteTestCacheProvider.withCache(fileURL: sqliteFileURL) { (cache) in - let store = ApolloStore(cache: cache) + let (cache, tearDown) = await SQLiteTestCacheProvider.makeNormalizedCache(fileURL: sqliteFileURL) + if let tearDown { self.addTeardownBlock(tearDown) } + let store = ApolloStore(cache: cache) - let server = MockGraphQLServer() - let networkTransport = MockNetworkTransport(server: server, store: store) + let server = MockGraphQLServer() + let networkTransport = MockNetworkTransport(server: server, store: store) - let client = ApolloClient(networkTransport: networkTransport, store: store) + let client = ApolloClient(networkTransport: networkTransport, store: store) - _ = server.expect(MockQuery.self) { request in - [ - "data": [ - "hero": [ - "name": "Luke Skywalker", - "__typename": "Human" - ] + _ = server.expect(MockQuery.self) { request in + [ + "data": [ + "hero": [ + "name": "Luke Skywalker", + "__typename": "Human" ] ] - } + ] + } + + let networkExpectation = self.expectation(description: "Fetching query from network") + let emptyCacheExpectation = self.expectation(description: "Fetch query from empty cache") + let cacheClearExpectation = self.expectation(description: "cache cleared") - let networkExpectation = self.expectation(description: "Fetching query from network") - let emptyCacheExpectation = self.expectation(description: "Fetch query from empty cache") - let cacheClearExpectation = self.expectation(description: "cache cleared") + client.fetch(query: query, cachePolicy: .fetchIgnoringCacheData) { outerResult in + defer { networkExpectation.fulfill() } - client.fetch(query: query, cachePolicy: .fetchIgnoringCacheData) { outerResult in - defer { networkExpectation.fulfill() } + switch outerResult { + case .failure(let error): + XCTFail("Unexpected failure: \(error)") + case .success(let graphQLResult): + XCTAssertEqual(graphQLResult.data?.hero?.name, "Luke Skywalker") + } - switch outerResult { + client.clearCache(completion: { result in + switch result { + case .success: + break case .failure(let error): - XCTFail("Unexpected failure: \(error)") - case .success(let graphQLResult): - XCTAssertEqual(graphQLResult.data?.hero?.name, "Luke Skywalker") + XCTFail("Error clearing cache: \(error)") } + cacheClearExpectation.fulfill() + }) - client.clearCache(completion: { result in - switch result { - case .success: - break - case .failure(let error): - XCTFail("Error clearing cache: \(error)") - } - cacheClearExpectation.fulfill() - }) + client.fetch(query: query, cachePolicy: .returnCacheDataDontFetch) { innerResult in + defer { emptyCacheExpectation.fulfill() } - client.fetch(query: query, cachePolicy: .returnCacheDataDontFetch) { innerResult in - defer { emptyCacheExpectation.fulfill() } - - switch innerResult { - case .success: - XCTFail("This should have returned an error") - case .failure(let error): - if let resultError = error as? JSONDecodingError { - switch resultError { - case .missingValue: - // Correct error! - break - default: - XCTFail("Unexpected JSON error: \(error)") - } - } else { - XCTFail("Unexpected error: \(error)") + switch innerResult { + case .success: + XCTFail("This should have returned an error") + case .failure(let error): + if let resultError = error as? JSONDecodingError { + switch resultError { + case .missingValue: + // Correct error! + break + default: + XCTFail("Unexpected JSON error: \(error)") } + } else { + XCTFail("Unexpected error: \(error)") } } } - - self.waitForExpectations(timeout: 2, handler: nil) } + + await fulfillment( + of: [networkExpectation, emptyCacheExpectation, cacheClearExpectation], + timeout: 2 + ) } } diff --git a/Tests/ApolloTests/Cache/StoreConcurrencyTests.swift b/Tests/ApolloTests/Cache/StoreConcurrencyTests.swift index 121ed462f..de22ddb92 100644 --- a/Tests/ApolloTests/Cache/StoreConcurrencyTests.swift +++ b/Tests/ApolloTests/Cache/StoreConcurrencyTests.swift @@ -14,10 +14,10 @@ class StoreConcurrencyTests: XCTestCase, CacheDependentTesting { var cache: (any NormalizedCache)! var store: ApolloStore! - override func setUpWithError() throws { - try super.setUpWithError() - - cache = try makeNormalizedCache() + override func setUp() async throws { + try await super.setUp() + + cache = try await makeNormalizedCache() store = ApolloStore(cache: cache) } diff --git a/Tests/ApolloTests/Cache/WatchQueryTests.swift b/Tests/ApolloTests/Cache/WatchQueryTests.swift index 38b05997d..367b634d6 100644 --- a/Tests/ApolloTests/Cache/WatchQueryTests.swift +++ b/Tests/ApolloTests/Cache/WatchQueryTests.swift @@ -16,10 +16,10 @@ class WatchQueryTests: XCTestCase, CacheDependentTesting { var server: MockGraphQLServer! var client: ApolloClient! - override func setUpWithError() throws { - try super.setUpWithError() - - cache = try makeNormalizedCache() + override func setUp() async throws { + try await super.setUp() + + cache = try await makeNormalizedCache() let store = ApolloStore(cache: cache) server = MockGraphQLServer()