From 167b71d936dd180597ffcf956c88ff1f1ab3b6a1 Mon Sep 17 00:00:00 2001 From: Caio Zullo Date: Wed, 29 Oct 2025 13:07:19 +0100 Subject: [PATCH 1/5] Introduce new CoreDataFeedStore async method and deprecate the sync one --- .../Infrastructure/CoreData/CoreDataFeedStore.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/EssentialFeed/EssentialFeed/Feed Cache/Infrastructure/CoreData/CoreDataFeedStore.swift b/EssentialFeed/EssentialFeed/Feed Cache/Infrastructure/CoreData/CoreDataFeedStore.swift index 93e57fb5..c99ce81f 100644 --- a/EssentialFeed/EssentialFeed/Feed Cache/Infrastructure/CoreData/CoreDataFeedStore.swift +++ b/EssentialFeed/EssentialFeed/Feed Cache/Infrastructure/CoreData/CoreDataFeedStore.swift @@ -45,6 +45,11 @@ public final class CoreDataFeedStore: Sendable { } } + public func perform(_ action: @escaping @Sendable () throws -> T) async rethrows -> T { + try await context.perform(action) + } + + @available(*, deprecated, message: "Use async version instead") public func perform(_ action: @Sendable @escaping () -> Void) { context.perform(action) } From fb1e44b05624ea737b12b95b3d3281edbffe5832 Mon Sep 17 00:00:00 2001 From: Caio Zullo Date: Wed, 29 Oct 2025 13:11:33 +0100 Subject: [PATCH 2/5] Migrate `CoreDataFeedStoreTests` to new async API --- .../Feed Cache/CoreDataFeedStoreTests.swift | 51 +++++++++---------- .../FeedStoreSpecs/FeedStoreSpecs.swift | 34 ++++++------- 2 files changed, 41 insertions(+), 44 deletions(-) diff --git a/EssentialFeed/EssentialFeedTests/Feed Cache/CoreDataFeedStoreTests.swift b/EssentialFeed/EssentialFeedTests/Feed Cache/CoreDataFeedStoreTests.swift index a85014d2..15c6c934 100644 --- a/EssentialFeed/EssentialFeedTests/Feed Cache/CoreDataFeedStoreTests.swift +++ b/EssentialFeed/EssentialFeedTests/Feed Cache/CoreDataFeedStoreTests.swift @@ -8,85 +8,82 @@ import EssentialFeed @MainActor class CoreDataFeedStoreTests: XCTestCase, FeedStoreSpecs { - func test_retrieve_deliversEmptyOnEmptyCache() throws { - try makeSUT { sut in + func test_retrieve_deliversEmptyOnEmptyCache() async throws { + try await makeSUT { sut in assertThatRetrieveDeliversEmptyOnEmptyCache(on: sut) } } - func test_retrieve_hasNoSideEffectsOnEmptyCache() throws { - try makeSUT { sut in + func test_retrieve_hasNoSideEffectsOnEmptyCache() async throws { + try await makeSUT { sut in assertThatRetrieveHasNoSideEffectsOnEmptyCache(on: sut) } } - func test_retrieve_deliversFoundValuesOnNonEmptyCache() throws { - try makeSUT { sut in + func test_retrieve_deliversFoundValuesOnNonEmptyCache() async throws { + try await makeSUT { sut in assertThatRetrieveDeliversFoundValuesOnNonEmptyCache(on: sut) } } - func test_retrieve_hasNoSideEffectsOnNonEmptyCache() throws { - try makeSUT { sut in + func test_retrieve_hasNoSideEffectsOnNonEmptyCache() async throws { + try await makeSUT { sut in assertThatRetrieveHasNoSideEffectsOnNonEmptyCache(on: sut) } } - func test_insert_deliversNoErrorOnEmptyCache() throws { - try makeSUT { sut in + func test_insert_deliversNoErrorOnEmptyCache() async throws { + try await makeSUT { sut in assertThatInsertDeliversNoErrorOnEmptyCache(on: sut) } } - func test_insert_deliversNoErrorOnNonEmptyCache() throws { - try makeSUT { sut in + func test_insert_deliversNoErrorOnNonEmptyCache() async throws { + try await makeSUT { sut in assertThatInsertDeliversNoErrorOnNonEmptyCache(on: sut) } } - func test_insert_overridesPreviouslyInsertedCacheValues() throws { - try makeSUT { sut in + func test_insert_overridesPreviouslyInsertedCacheValues() async throws { + try await makeSUT { sut in assertThatInsertOverridesPreviouslyInsertedCacheValues(on: sut) } } - func test_delete_deliversNoErrorOnEmptyCache() throws { - try makeSUT { sut in + func test_delete_deliversNoErrorOnEmptyCache() async throws { + try await makeSUT { sut in assertThatDeleteDeliversNoErrorOnEmptyCache(on: sut) } } - func test_delete_hasNoSideEffectsOnEmptyCache() throws { - try makeSUT { sut in + func test_delete_hasNoSideEffectsOnEmptyCache() async throws { + try await makeSUT { sut in assertThatDeleteHasNoSideEffectsOnEmptyCache(on: sut) } } - func test_delete_deliversNoErrorOnNonEmptyCache() throws { - try makeSUT { sut in + func test_delete_deliversNoErrorOnNonEmptyCache() async throws { + try await makeSUT { sut in assertThatDeleteDeliversNoErrorOnNonEmptyCache(on: sut) } } - func test_delete_emptiesPreviouslyInsertedCache() throws { - try makeSUT { sut in + func test_delete_emptiesPreviouslyInsertedCache() async throws { + try await makeSUT { sut in assertThatDeleteEmptiesPreviouslyInsertedCache(on: sut) } } // - MARK: Helpers - private func makeSUT(_ test: @Sendable @escaping (CoreDataFeedStore) -> Void, file: StaticString = #filePath, line: UInt = #line) throws { + private func makeSUT(_ test: @Sendable @escaping (CoreDataFeedStore) -> Void, file: StaticString = #filePath, line: UInt = #line) async throws { let storeURL = URL(fileURLWithPath: "/dev/null") let sut = try CoreDataFeedStore(storeURL: storeURL) trackForMemoryLeaks(sut, file: file, line: line) - let exp = expectation(description: "wait for operation") - sut.perform { + await sut.perform { test(sut) - exp.fulfill() } - wait(for: [exp], timeout: 0.1) } } diff --git a/EssentialFeed/EssentialFeedTests/Feed Cache/FeedStoreSpecs/FeedStoreSpecs.swift b/EssentialFeed/EssentialFeedTests/Feed Cache/FeedStoreSpecs/FeedStoreSpecs.swift index 29c2650f..5d8392e8 100644 --- a/EssentialFeed/EssentialFeedTests/Feed Cache/FeedStoreSpecs/FeedStoreSpecs.swift +++ b/EssentialFeed/EssentialFeedTests/Feed Cache/FeedStoreSpecs/FeedStoreSpecs.swift @@ -5,34 +5,34 @@ import Foundation protocol FeedStoreSpecs { - func test_retrieve_deliversEmptyOnEmptyCache() throws - func test_retrieve_hasNoSideEffectsOnEmptyCache() throws - func test_retrieve_deliversFoundValuesOnNonEmptyCache() throws - func test_retrieve_hasNoSideEffectsOnNonEmptyCache() throws + func test_retrieve_deliversEmptyOnEmptyCache() async throws + func test_retrieve_hasNoSideEffectsOnEmptyCache() async throws + func test_retrieve_deliversFoundValuesOnNonEmptyCache() async throws + func test_retrieve_hasNoSideEffectsOnNonEmptyCache() async throws - func test_insert_deliversNoErrorOnEmptyCache() throws - func test_insert_deliversNoErrorOnNonEmptyCache() throws - func test_insert_overridesPreviouslyInsertedCacheValues() throws + func test_insert_deliversNoErrorOnEmptyCache() async throws + func test_insert_deliversNoErrorOnNonEmptyCache() async throws + func test_insert_overridesPreviouslyInsertedCacheValues() async throws - func test_delete_deliversNoErrorOnEmptyCache() throws - func test_delete_hasNoSideEffectsOnEmptyCache() throws - func test_delete_deliversNoErrorOnNonEmptyCache() throws - func test_delete_emptiesPreviouslyInsertedCache() throws + func test_delete_deliversNoErrorOnEmptyCache() async throws + func test_delete_hasNoSideEffectsOnEmptyCache() async throws + func test_delete_deliversNoErrorOnNonEmptyCache() async throws + func test_delete_emptiesPreviouslyInsertedCache() async throws } protocol FailableRetrieveFeedStoreSpecs: FeedStoreSpecs { - func test_retrieve_deliversFailureOnRetrievalError() throws - func test_retrieve_hasNoSideEffectsOnFailure() throws + func test_retrieve_deliversFailureOnRetrievalError() async throws + func test_retrieve_hasNoSideEffectsOnFailure() async throws } protocol FailableInsertFeedStoreSpecs: FeedStoreSpecs { - func test_insert_deliversErrorOnInsertionError() throws - func test_insert_hasNoSideEffectsOnInsertionError() throws + func test_insert_deliversErrorOnInsertionError() async throws + func test_insert_hasNoSideEffectsOnInsertionError() async throws } protocol FailableDeleteFeedStoreSpecs: FeedStoreSpecs { - func test_delete_deliversErrorOnDeletionError() throws - func test_delete_hasNoSideEffectsOnDeletionError() throws + func test_delete_deliversErrorOnDeletionError() async throws + func test_delete_hasNoSideEffectsOnDeletionError() async throws } typealias FailableFeedStoreSpecs = FailableRetrieveFeedStoreSpecs & FailableInsertFeedStoreSpecs & FailableDeleteFeedStoreSpecs From c8ef13cc5aa8ff2b0d64062d7d10640270d0d820 Mon Sep 17 00:00:00 2001 From: Caio Zullo Date: Wed, 29 Oct 2025 13:12:41 +0100 Subject: [PATCH 3/5] Migrate `CoreDataFeedImageDataStoreTests` to new async API --- .../CoreDataFeedImageDataStoreTests.swift | 23 ++++++++----------- .../FeedImageDataStoreSpecs.swift | 8 +++---- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/EssentialFeed/EssentialFeedTests/Feed Cache/CoreDataFeedImageDataStoreTests.swift b/EssentialFeed/EssentialFeedTests/Feed Cache/CoreDataFeedImageDataStoreTests.swift index 6c22f11f..69cea842 100644 --- a/EssentialFeed/EssentialFeedTests/Feed Cache/CoreDataFeedImageDataStoreTests.swift +++ b/EssentialFeed/EssentialFeedTests/Feed Cache/CoreDataFeedImageDataStoreTests.swift @@ -8,45 +8,42 @@ import EssentialFeed @MainActor class CoreDataFeedImageDataStoreTests: XCTestCase, FeedImageDataStoreSpecs { - func test_retrieveImageData_deliversNotFoundWhenEmpty() throws { - try makeSUT { sut, imageDataURL in + func test_retrieveImageData_deliversNotFoundWhenEmpty() async throws { + try await makeSUT { sut, imageDataURL in assertThatRetrieveImageDataDeliversNotFoundOnEmptyCache(on: sut, imageDataURL: imageDataURL) } } - func test_retrieveImageData_deliversNotFoundWhenStoredDataURLDoesNotMatch() throws { - try makeSUT { sut, imageDataURL in + func test_retrieveImageData_deliversNotFoundWhenStoredDataURLDoesNotMatch() async throws { + try await makeSUT { sut, imageDataURL in assertThatRetrieveImageDataDeliversNotFoundWhenStoredDataURLDoesNotMatch(on: sut, imageDataURL: imageDataURL) } } - func test_retrieveImageData_deliversFoundDataWhenThereIsAStoredImageDataMatchingURL() throws { - try makeSUT { sut, imageDataURL in + func test_retrieveImageData_deliversFoundDataWhenThereIsAStoredImageDataMatchingURL() async throws { + try await makeSUT { sut, imageDataURL in assertThatRetrieveImageDataDeliversFoundDataWhenThereIsAStoredImageDataMatchingURL(on: sut, imageDataURL: imageDataURL) } } - func test_retrieveImageData_deliversLastInsertedValue() throws { - try makeSUT { sut, imageDataURL in + func test_retrieveImageData_deliversLastInsertedValue() async throws { + try await makeSUT { sut, imageDataURL in assertThatRetrieveImageDataDeliversLastInsertedValueForURL(on: sut, imageDataURL: imageDataURL) } } // - MARK: Helpers - private func makeSUT(_ test: @Sendable @escaping (CoreDataFeedStore, URL) -> Void, file: StaticString = #filePath, line: UInt = #line) throws { + private func makeSUT(_ test: @Sendable @escaping (CoreDataFeedStore, URL) -> Void, file: StaticString = #filePath, line: UInt = #line) async throws { let storeURL = URL(fileURLWithPath: "/dev/null") let sut = try CoreDataFeedStore(storeURL: storeURL) trackForMemoryLeaks(sut, file: file, line: line) - let exp = expectation(description: "wait for operation") - sut.perform { + await sut.perform { let imageDataURL = URL(string: "http://a-url.com")! insertFeedImage(with: imageDataURL, into: sut, file: file, line: line) test(sut, imageDataURL) - exp.fulfill() } - wait(for: [exp], timeout: 0.1) } } diff --git a/EssentialFeed/EssentialFeedTests/Feed Cache/FeedImageDataStoreSpecs/FeedImageDataStoreSpecs.swift b/EssentialFeed/EssentialFeedTests/Feed Cache/FeedImageDataStoreSpecs/FeedImageDataStoreSpecs.swift index 55f3d47b..5ebaeed1 100644 --- a/EssentialFeed/EssentialFeedTests/Feed Cache/FeedImageDataStoreSpecs/FeedImageDataStoreSpecs.swift +++ b/EssentialFeed/EssentialFeedTests/Feed Cache/FeedImageDataStoreSpecs/FeedImageDataStoreSpecs.swift @@ -5,8 +5,8 @@ import Foundation protocol FeedImageDataStoreSpecs { - func test_retrieveImageData_deliversNotFoundWhenEmpty() throws - func test_retrieveImageData_deliversNotFoundWhenStoredDataURLDoesNotMatch() throws - func test_retrieveImageData_deliversFoundDataWhenThereIsAStoredImageDataMatchingURL() throws - func test_retrieveImageData_deliversLastInsertedValue() throws + func test_retrieveImageData_deliversNotFoundWhenEmpty() async throws + func test_retrieveImageData_deliversNotFoundWhenStoredDataURLDoesNotMatch() async throws + func test_retrieveImageData_deliversFoundDataWhenThereIsAStoredImageDataMatchingURL() async throws + func test_retrieveImageData_deliversLastInsertedValue() async throws } From 47582d4d7036e7e69acfb8623d61b4774a6f6fc9 Mon Sep 17 00:00:00 2001 From: Caio Zullo Date: Wed, 29 Oct 2025 13:16:47 +0100 Subject: [PATCH 4/5] Migrate `CoreDataFeedStoreScheduler` to new async API --- EssentialApp/EssentialApp/CombineHelpers.swift | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/EssentialApp/EssentialApp/CombineHelpers.swift b/EssentialApp/EssentialApp/CombineHelpers.swift index 8cb3ca2a..1198596b 100644 --- a/EssentialApp/EssentialApp/CombineHelpers.swift +++ b/EssentialApp/EssentialApp/CombineHelpers.swift @@ -227,6 +227,7 @@ extension AnyDispatchQueueScheduler { CoreDataFeedStoreScheduler(store: store).eraseToAnyScheduler() } + @MainActor private struct CoreDataFeedStoreScheduler: Scheduler { let store: CoreDataFeedStore @@ -239,7 +240,9 @@ extension AnyDispatchQueueScheduler { action() } else { nonisolated(unsafe) let uncheckedAction = action - store.perform { uncheckedAction() } + Task.immediate { + await store.perform { uncheckedAction() } + } } return AnyCancellable {} } @@ -249,7 +252,9 @@ extension AnyDispatchQueueScheduler { action() } else { nonisolated(unsafe) let uncheckedAction = action - store.perform { uncheckedAction() } + Task.immediate { + await store.perform { uncheckedAction() } + } } } @@ -258,7 +263,9 @@ extension AnyDispatchQueueScheduler { action() } else { nonisolated(unsafe) let uncheckedAction = action - store.perform { uncheckedAction() } + Task.immediate { + await store.perform { uncheckedAction() } + } } } } From 624984efa40c9ba449f6fa5e92e9ca967d20f9bf Mon Sep 17 00:00:00 2001 From: Caio Zullo Date: Wed, 29 Oct 2025 13:17:45 +0100 Subject: [PATCH 5/5] Remove deprecated `CoreDataFeedStore` sync API in favor of the new async API --- .../Infrastructure/CoreData/CoreDataFeedStore.swift | 5 ----- 1 file changed, 5 deletions(-) diff --git a/EssentialFeed/EssentialFeed/Feed Cache/Infrastructure/CoreData/CoreDataFeedStore.swift b/EssentialFeed/EssentialFeed/Feed Cache/Infrastructure/CoreData/CoreDataFeedStore.swift index c99ce81f..0b24df09 100644 --- a/EssentialFeed/EssentialFeed/Feed Cache/Infrastructure/CoreData/CoreDataFeedStore.swift +++ b/EssentialFeed/EssentialFeed/Feed Cache/Infrastructure/CoreData/CoreDataFeedStore.swift @@ -49,11 +49,6 @@ public final class CoreDataFeedStore: Sendable { try await context.perform(action) } - @available(*, deprecated, message: "Use async version instead") - public func perform(_ action: @Sendable @escaping () -> Void) { - context.perform(action) - } - private func cleanUpReferencesToPersistentStores() { context.performAndWait { let coordinator = self.container.persistentStoreCoordinator