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() } + } } } } diff --git a/EssentialFeed/EssentialFeed/Feed Cache/Infrastructure/CoreData/CoreDataFeedStore.swift b/EssentialFeed/EssentialFeed/Feed Cache/Infrastructure/CoreData/CoreDataFeedStore.swift index 93e57fb5..0b24df09 100644 --- a/EssentialFeed/EssentialFeed/Feed Cache/Infrastructure/CoreData/CoreDataFeedStore.swift +++ b/EssentialFeed/EssentialFeed/Feed Cache/Infrastructure/CoreData/CoreDataFeedStore.swift @@ -45,8 +45,8 @@ public final class CoreDataFeedStore: Sendable { } } - public func perform(_ action: @Sendable @escaping () -> Void) { - context.perform(action) + public func perform(_ action: @escaping @Sendable () throws -> T) async rethrows -> T { + try await context.perform(action) } private func cleanUpReferencesToPersistentStores() { 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/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/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 } 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