diff --git a/Development/TinkoffConcurrency/AsyncQueue/TCAsyncQueue.swift b/Development/TinkoffConcurrency/AsyncQueue/TCAsyncQueue.swift new file mode 100644 index 0000000..10f4235 --- /dev/null +++ b/Development/TinkoffConcurrency/AsyncQueue/TCAsyncQueue.swift @@ -0,0 +1,63 @@ +public final actor TCAsyncQueue { + + // MARK: - Dependencies + + private let taskFactory: ITCTaskFactory + + // MARK: - Private Properties + + private var lastEnqueuedTask: ITask? + + // MARK: - Initializers + + public init(taskFactory: ITCTaskFactory = TCTaskFactory()) { + self.taskFactory = taskFactory + } + + // MARK: - Methods + + @discardableResult + public func enqueue(operation: @escaping @Sendable () async -> T) -> Task { + let lastEnqueuedTask = lastEnqueuedTask + + let task = taskFactory.task { + await lastEnqueuedTask?.wait() + + return await operation() + } + + self.lastEnqueuedTask = task + + return task + } + + @discardableResult + public func enqueue(operation: @escaping @Sendable () async throws -> T) -> Task { + let lastEnqueuedTask = lastEnqueuedTask + + let task = taskFactory.task { + await lastEnqueuedTask?.wait() + + return try await operation() + } + + self.lastEnqueuedTask = task + + return task + } +} + +extension TCAsyncQueue { + + // MARK: - Methods + + public func perform(operation: @escaping @Sendable () async throws -> T) async rethrows -> T { + let task = enqueue(operation: operation) + + return try await withTaskCancellationHandler { + try await task.value + } onCancel: { + task.cancel() + } + } +} diff --git a/Development/TinkoffConcurrency/Internals/ITask.swift b/Development/TinkoffConcurrency/Internals/ITask.swift new file mode 100644 index 0000000..45c1b8c --- /dev/null +++ b/Development/TinkoffConcurrency/Internals/ITask.swift @@ -0,0 +1,15 @@ +protocol ITask { + + // MARK: - Methods + + func wait() async +} + +extension Task: ITask { + + // MARK: - ITask + + func wait() async { + _ = await result + } +} diff --git a/Example/Podfile.lock b/Example/Podfile.lock index 23a26b1..d81eb8c 100644 --- a/Example/Podfile.lock +++ b/Example/Podfile.lock @@ -1,6 +1,7 @@ PODS: - TinkoffConcurrency (1.2.0) - - TinkoffConcurrency/Tests (1.2.0) + - TinkoffConcurrency/Tests (1.2.0): + - TinkoffConcurrencyTesting (~> 1.2.0) - TinkoffConcurrencyTesting (1.2.0): - TinkoffConcurrency (~> 1.2.0) - TinkoffConcurrencyTesting/Tests (1.2.0): @@ -19,7 +20,7 @@ EXTERNAL SOURCES: :path: "../" SPEC CHECKSUMS: - TinkoffConcurrency: c733c4635a33a074bc64e1d72e03ba7d5affd4c7 + TinkoffConcurrency: a63843ad0a598fa151732a5738ba2109049cc8d4 TinkoffConcurrencyTesting: ddddbefe4e962b54464ce2069db26e6558e51e47 PODFILE CHECKSUM: 59bbb119a3817973bcf62c31b9a94125545be248 diff --git a/Tests/TinkoffConcurrency/AsyncQueue/TCAsyncQueueTests.swift b/Tests/TinkoffConcurrency/AsyncQueue/TCAsyncQueueTests.swift new file mode 100644 index 0000000..5fda27a --- /dev/null +++ b/Tests/TinkoffConcurrency/AsyncQueue/TCAsyncQueueTests.swift @@ -0,0 +1,167 @@ +import XCTest + +import TinkoffConcurrency +import TinkoffConcurrencyTesting + +final class TCAsyncQueueTests: XCTestCase { + + // MARK: - Dependencies + + private var taskFactory: TCTestTaskFactory! + + // MARK: - XCTestCase + + override func setUp() { + super.setUp() + + taskFactory = TCTestTaskFactory() + } + + // MARK: - Tests + + func test_asyncQueue_enqueue_order() async throws { + // given + let expectation1 = expectation(description: "operation 1") + let expectation2 = expectation(description: "operation 2") + let expectation3 = expectation(description: "operation 3") + + let result = UncheckedSendable([Int]()) + + let queue = TCAsyncQueue(taskFactory: taskFactory) + + // throwing operation that throws + await queue.enqueue { + await XCTWaiter.waitAsync(for: [expectation1], timeout: 1) + + result.mutate { $0.append(1) } + + throw FakeErrors.default + } + + // throwing operation that returns value + await queue.enqueue { + await XCTWaiter.waitAsync(for: [expectation2], timeout: 1) + + try throwingHelper() + + result.mutate { $0.append(2) } + } + + // non-throwing operation + await queue.enqueue { + await XCTWaiter.waitAsync(for: [expectation3], timeout: 1) + + result.mutate { $0.append(3) } + } + + // when + expectation3.fulfill() + expectation2.fulfill() + expectation1.fulfill() + + await taskFactory.runUntilIdle() + + // then + XCTAssertEqual(result.value, [1, 2, 3]) + } + + func test_asyncQueue_enqueue_result() async throws { + // given + let queue = TCAsyncQueue(taskFactory: taskFactory) + + let queueEnqueueResult = String.fake() + + // when + let task = await queue.enqueue { + queueEnqueueResult + } + + await taskFactory.runUntilIdle() + + let result = await task.value + + // then + XCTAssertEqual(result, queueEnqueueResult) + } + + func test_asyncQueue_throwingEnqueue_result() async throws { + // given + let queue = TCAsyncQueue(taskFactory: taskFactory) + + let queueEnqueueResult = String.fake() + + // when + let task = await queue.enqueue { + try throwingHelper() + return queueEnqueueResult + } + + await taskFactory.runUntilIdle() + + let result = try await task.value + + // then + XCTAssertEqual(result, queueEnqueueResult) + } + + func test_asyncQueue_throwingEnqueue_throwing() async throws { + // given + let queue = TCAsyncQueue(taskFactory: taskFactory) + + let queueEnqueueResult = FakeErrors.default + + // when + let task = await queue.enqueue { + throw queueEnqueueResult + } + + await taskFactory.runUntilIdle() + + let result = await XCTExecuteThrowsError(try await task.value)! + + // then + XCTAssertEqualErrors(result, queueEnqueueResult) + } + + func test_asyncQueue_perform() async throws { + // given + let queue = TCAsyncQueue(taskFactory: taskFactory) + + let queueEnqueueResult = String.fake() + + // when + let result = await queue.perform { + queueEnqueueResult + } + + await taskFactory.runUntilIdle() + + // then + XCTAssertEqual(result, queueEnqueueResult) + } + + func test_asyncQueue_perform_cancel() async throws { + // given + let queue = TCAsyncQueue(taskFactory: taskFactory) + + let expectation = expectation(description: "operation started") + + // when + let task = Task { + await XCTWaiter.waitAsync(for: [expectation], timeout: 1) + + await queue.perform { + // then + XCTAssertTrue(Task.isCancelled) + } + } + + task.cancel() + + expectation.fulfill() + + await taskFactory.runUntilIdle() + } +} + +private func throwingHelper() throws {} diff --git a/TinkoffConcurrency.podspec b/TinkoffConcurrency.podspec index 69001b6..6669465 100644 --- a/TinkoffConcurrency.podspec +++ b/TinkoffConcurrency.podspec @@ -25,6 +25,8 @@ Pod::Spec.new do |s| s.source_files = 'Development/TinkoffConcurrency/**/*.{swift,md,docc}' s.test_spec 'Tests' do |test_spec| + test_spec.dependency 'TinkoffConcurrencyTesting', '~> 1.2.0' + test_spec.source_files = ["Tests/TinkoffConcurrency/**/*.swift", "Tests/TestSupport/**/*.swift"] end end