diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index feac9ad..ce3d300 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -6,15 +6,16 @@ on: jobs: build: - runs-on: macos-13 + runs-on: macos-14 steps: - - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: latest-stable - - name: Check XCode Version + - uses: actions/checkout@v4 + - name: List available Xcode versions + run: ls /Applications | grep Xcode + - name: Set up Xcode version + run: sudo xcode-select -s /Applications/Xcode_16.0.app/Contents/Developer + - name: Show current version of Xcode run: xcodebuild -version - - uses: actions/checkout@v3 - name: Build run: xcodebuild -scheme SwiftRetrier build -destination "platform=OS X" - name: Run tests @@ -22,7 +23,7 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 - name: SwiftLint uses: raphaelbussa/swiftlint-action@main with: diff --git a/Package.swift b/Package.swift index fcf16f0..e165fe4 100644 --- a/Package.swift +++ b/Package.swift @@ -35,5 +35,6 @@ let package = Package( dependencies: ["SwiftRetrier"], plugins: [] + extraPlugins ) - ] + ], + swiftLanguageVersions: [.version("6")] ) diff --git a/Sources/SwiftRetrier/Core/ConstantDelayPolicy/ConstantDelayRetryPolicy.swift b/Sources/SwiftRetrier/Core/ConstantDelayPolicy/ConstantDelayRetryPolicy.swift index 0ae77ad..885885c 100644 --- a/Sources/SwiftRetrier/Core/ConstantDelayPolicy/ConstantDelayRetryPolicy.swift +++ b/Sources/SwiftRetrier/Core/ConstantDelayPolicy/ConstantDelayRetryPolicy.swift @@ -1,6 +1,6 @@ import Foundation -open class ConstantDelayRetryPolicy: RetryPolicy { +public struct ConstantDelayRetryPolicy: RetryPolicy { public let delay: TimeInterval @@ -16,7 +16,7 @@ open class ConstantDelayRetryPolicy: RetryPolicy { .retry(delay: retryDelay(for: attemptFailure)) } - public func freshCopy() -> RetryPolicy { + public func policyAfter(attemptFailure: AttemptFailure, delay: TimeInterval) -> any RetryPolicy { self } } diff --git a/Sources/SwiftRetrier/Core/ExponentialBackoffPolicy/ExponentialBackoffRetryPolicy.swift b/Sources/SwiftRetrier/Core/ExponentialBackoffPolicy/ExponentialBackoffRetryPolicy.swift index 03382ae..fe79baf 100644 --- a/Sources/SwiftRetrier/Core/ExponentialBackoffPolicy/ExponentialBackoffRetryPolicy.swift +++ b/Sources/SwiftRetrier/Core/ExponentialBackoffPolicy/ExponentialBackoffRetryPolicy.swift @@ -1,8 +1,8 @@ import Foundation -open class ExponentialBackoffRetryPolicy: RetryPolicy { +public struct ExponentialBackoffRetryPolicy: RetryPolicy { - public enum Jitter { + public enum Jitter: Sendable { case none case full case decorrelated(growthFactor: Double = ExponentialBackoffConstants.defaultDecorrelatedJitterGrowthFactor) @@ -11,14 +11,16 @@ open class ExponentialBackoffRetryPolicy: RetryPolicy { public let timeSlot: TimeInterval public let maxDelay: TimeInterval public let jitter: Jitter - private var previousDelay: TimeInterval? + private let previousDelay: TimeInterval? public init(timeSlot: TimeInterval = ExponentialBackoffConstants.defaultTimeSlot, maxDelay: TimeInterval = ExponentialBackoffConstants.defaultMaxDelay, - jitter: Jitter = ExponentialBackoffConstants.defaultJitter) { + jitter: Jitter = ExponentialBackoffConstants.defaultJitter, + previousDelay: TimeInterval? = nil) { self.timeSlot = timeSlot self.maxDelay = maxDelay self.jitter = jitter + self.previousDelay = previousDelay } public func exponentiationBySquaring(_ base: T, _ multiplier: T, _ exponent: T) -> T { @@ -57,7 +59,6 @@ open class ExponentialBackoffRetryPolicy: RetryPolicy { } else { delay = fullJitterDelay(attemptIndex: attemptIndex) } - previousDelay = delay return delay } @@ -72,7 +73,7 @@ open class ExponentialBackoffRetryPolicy: RetryPolicy { } } - open func retryDelay(for attemptFailure: AttemptFailure) -> TimeInterval { + public func retryDelay(for attemptFailure: AttemptFailure) -> TimeInterval { min(maxDelay, uncappedDelay(attemptIndex: attemptFailure.index)) } @@ -80,7 +81,7 @@ open class ExponentialBackoffRetryPolicy: RetryPolicy { .retry(delay: retryDelay(for: attemptFailure)) } - public func freshCopy() -> RetryPolicy { - ExponentialBackoffRetryPolicy(timeSlot: timeSlot, maxDelay: maxDelay, jitter: jitter) + public func policyAfter(attemptFailure: AttemptFailure, delay: TimeInterval) -> any RetryPolicy { + ExponentialBackoffRetryPolicy(timeSlot: timeSlot, maxDelay: maxDelay, jitter: jitter, previousDelay: delay) } } diff --git a/Sources/SwiftRetrier/Core/Model/Job.swift b/Sources/SwiftRetrier/Core/Model/Job.swift index 7aaab44..30e0ad3 100644 --- a/Sources/SwiftRetrier/Core/Model/Job.swift +++ b/Sources/SwiftRetrier/Core/Model/Job.swift @@ -1 +1 @@ -public typealias Job = () async throws -> Value +public typealias Job = @Sendable () async throws -> Value diff --git a/Sources/SwiftRetrier/Core/Model/Policies/AttemptFailure.swift b/Sources/SwiftRetrier/Core/Model/Policies/AttemptFailure.swift index 59dfecc..f118367 100644 --- a/Sources/SwiftRetrier/Core/Model/Policies/AttemptFailure.swift +++ b/Sources/SwiftRetrier/Core/Model/Policies/AttemptFailure.swift @@ -1,6 +1,6 @@ import Foundation -public struct AttemptFailure { +public struct AttemptFailure: Sendable { public let trialStart: Date public let index: UInt public let error: Error diff --git a/Sources/SwiftRetrier/Core/Model/Policies/RetryDecision.swift b/Sources/SwiftRetrier/Core/Model/Policies/RetryDecision.swift index 32fcda4..c3e9204 100644 --- a/Sources/SwiftRetrier/Core/Model/Policies/RetryDecision.swift +++ b/Sources/SwiftRetrier/Core/Model/Policies/RetryDecision.swift @@ -1,6 +1,6 @@ import Foundation -public enum RetryDecision { +public enum RetryDecision: Sendable { case giveUp case retry(delay: TimeInterval) } diff --git a/Sources/SwiftRetrier/Core/Model/Policies/RetryPolicy.swift b/Sources/SwiftRetrier/Core/Model/Policies/RetryPolicy.swift index 889c9ee..21d20c7 100644 --- a/Sources/SwiftRetrier/Core/Model/Policies/RetryPolicy.swift +++ b/Sources/SwiftRetrier/Core/Model/Policies/RetryPolicy.swift @@ -1,7 +1,7 @@ import Foundation -public protocol RetryPolicy { +public protocol RetryPolicy: Sendable { func shouldRetry(on attemptFailure: AttemptFailure) -> RetryDecision func retryDelay(for attemptFailure: AttemptFailure) -> TimeInterval - func freshCopy() -> RetryPolicy + func policyAfter(attemptFailure: AttemptFailure, delay: TimeInterval) -> any RetryPolicy } diff --git a/Sources/SwiftRetrier/Core/Model/Retriers/Retrier.swift b/Sources/SwiftRetrier/Core/Model/Retriers/Retrier.swift index c0ca8ce..1151170 100644 --- a/Sources/SwiftRetrier/Core/Model/Retriers/Retrier.swift +++ b/Sources/SwiftRetrier/Core/Model/Retriers/Retrier.swift @@ -1,8 +1,8 @@ import Foundation import Combine -public protocol Retrier: Cancellable, AnyObject { - associatedtype Output +public protocol Retrier: Cancellable, AnyObject, Sendable { + associatedtype Output: Sendable func publisher() -> AnyPublisher, Never> } diff --git a/Sources/SwiftRetrier/Core/PolicyBuilding/GiveUpOnPolicyWrapper.swift b/Sources/SwiftRetrier/Core/PolicyBuilding/GiveUpOnPolicyWrapper.swift index 6204e83..cc73128 100644 --- a/Sources/SwiftRetrier/Core/PolicyBuilding/GiveUpOnPolicyWrapper.swift +++ b/Sources/SwiftRetrier/Core/PolicyBuilding/GiveUpOnPolicyWrapper.swift @@ -3,9 +3,9 @@ import Foundation public struct GiveUpOnPolicyWrapper: RetryPolicy { private let wrapped: RetryPolicy - private let giveUpCriterium: (AttemptFailure) -> Bool + private let giveUpCriterium: @Sendable (AttemptFailure) -> Bool - public init(wrapped: RetryPolicy, giveUpCriterium: @escaping (AttemptFailure) -> Bool) { + public init(wrapped: RetryPolicy, giveUpCriterium: @escaping @Sendable (AttemptFailure) -> Bool) { self.wrapped = wrapped self.giveUpCriterium = giveUpCriterium } @@ -21,7 +21,10 @@ public struct GiveUpOnPolicyWrapper: RetryPolicy { return wrapped.shouldRetry(on: attemptFailure) } - public func freshCopy() -> RetryPolicy { - GiveUpOnPolicyWrapper(wrapped: wrapped.freshCopy(), giveUpCriterium: giveUpCriterium) + public func policyAfter(attemptFailure: AttemptFailure, delay: TimeInterval) -> any RetryPolicy { + GiveUpOnPolicyWrapper( + wrapped: wrapped.policyAfter(attemptFailure: attemptFailure, delay: delay), + giveUpCriterium: giveUpCriterium + ) } } diff --git a/Sources/SwiftRetrier/Core/PolicyBuilding/RetryPolicy+GiveUpOn.swift b/Sources/SwiftRetrier/Core/PolicyBuilding/RetryPolicy+GiveUpOn.swift index 8413fec..70e80d2 100644 --- a/Sources/SwiftRetrier/Core/PolicyBuilding/RetryPolicy+GiveUpOn.swift +++ b/Sources/SwiftRetrier/Core/PolicyBuilding/RetryPolicy+GiveUpOn.swift @@ -2,7 +2,7 @@ import Foundation public extension RetryPolicy { - func giveUp(on giveUpCriterium: @escaping (AttemptFailure) -> Bool) -> RetryPolicy { + func giveUp(on giveUpCriterium: @escaping @Sendable (AttemptFailure) -> Bool) -> RetryPolicy { GiveUpOnPolicyWrapper(wrapped: self, giveUpCriterium: giveUpCriterium) } @@ -17,7 +17,7 @@ public extension RetryPolicy { }) } - func giveUpOnErrors(matching finalErrorCriterium: @escaping (Error) -> Bool) -> RetryPolicy { + func giveUpOnErrors(matching finalErrorCriterium: @escaping @Sendable (Error) -> Bool) -> RetryPolicy { GiveUpOnPolicyWrapper(wrapped: self, giveUpCriterium: { finalErrorCriterium($0.error) }) } } diff --git a/Sources/SwiftRetrier/Core/Retriers/ConditionalRetrier.swift b/Sources/SwiftRetrier/Core/Retriers/ConditionalRetrier.swift index 175d589..a96ac25 100644 --- a/Sources/SwiftRetrier/Core/Retriers/ConditionalRetrier.swift +++ b/Sources/SwiftRetrier/Core/Retriers/ConditionalRetrier.swift @@ -20,7 +20,7 @@ import Combine /// /// If the condition publisher completes and it had not emitted any value or the last value it emitted was `false` /// then the retrier emits a completion embedding `RetryError.conditionPublisherCompleted` and finishes. -public class ConditionalRetrier: SingleOutputRetrier { +public class ConditionalRetrier: SingleOutputRetrier, @unchecked Sendable { private let policy: RetryPolicy private let job: Job diff --git a/Sources/SwiftRetrier/Core/Retriers/Repeater.swift b/Sources/SwiftRetrier/Core/Retriers/Repeater.swift index 65ef724..e7e2ad7 100644 --- a/Sources/SwiftRetrier/Core/Retriers/Repeater.swift +++ b/Sources/SwiftRetrier/Core/Retriers/Repeater.swift @@ -22,7 +22,7 @@ import Combine /// ``` /// /// On cancellation, the publisher emits a completion embedding a `CancellationError`then finishes. -public class Repeater: Retrier { +public class Repeater: Retrier, @unchecked Sendable { private let retrierBuilder: () -> AnySingleOutputRetrier private var retrier: AnySingleOutputRetrier? diff --git a/Sources/SwiftRetrier/Core/Retriers/SimpleRetrier.swift b/Sources/SwiftRetrier/Core/Retriers/SimpleRetrier.swift index 59e9d1d..d9dcbef 100644 --- a/Sources/SwiftRetrier/Core/Retriers/SimpleRetrier.swift +++ b/Sources/SwiftRetrier/Core/Retriers/SimpleRetrier.swift @@ -10,14 +10,14 @@ import Combine /// the last attempt error, the publisher emits the attempt failure then a completion embedding the attempt error. /// - **the retrier is canceled:** any awaiting on the `value` property will throw a `CancellationError`, the publisher /// emits a completion embedding the same error then finishes. -public class SimpleRetrier: SingleOutputRetrier { +public class SimpleRetrier: SingleOutputRetrier, @unchecked Sendable { public let trialStart = Date() private let subject = PassthroughSubject, Never>() private var task: Task! public init(policy: RetryPolicy, job: @escaping Job) { - self.task = createTask(policy: policy.freshCopy(), job: job) + self.task = createTask(policy: policy, job: job) } @MainActor @@ -49,6 +49,7 @@ public class SimpleRetrier: SingleOutputRetrier { // Ensure we don't start before any ongoing business on main actor is finished await MainActor.run {} do { + var policy = policy var attemptIndex: UInt = 0 while true { try Task.checkCancellation() @@ -60,7 +61,7 @@ public class SimpleRetrier: SingleOutputRetrier { let attemptFailure = AttemptFailure(trialStart: trialStart, index: attemptIndex, error: error) await sendAttemptFailure(attemptFailure) try Task.checkCancellation() - let retryDecision = await MainActor.run { [attemptIndex] in + let retryDecision = await MainActor.run { [policy, attemptIndex] in policy.shouldRetry(on: AttemptFailure(trialStart: trialStart, index: attemptIndex, error: error)) @@ -70,6 +71,7 @@ public class SimpleRetrier: SingleOutputRetrier { throw error case .retry(delay: let delay): try await Task.sleep(nanoseconds: nanoseconds(delay)) + policy = policy.policyAfter(attemptFailure: attemptFailure, delay: delay) attemptIndex += 1 } } diff --git a/Sources/SwiftRetrier/DSL/ColdRepeater.swift b/Sources/SwiftRetrier/DSL/ColdRepeater.swift index 23f1618..eedd4ee 100644 --- a/Sources/SwiftRetrier/DSL/ColdRepeater.swift +++ b/Sources/SwiftRetrier/DSL/ColdRepeater.swift @@ -9,7 +9,7 @@ public struct ColdRepeater { public extension ColdRepeater { - func giveUp(on giveUpCriterium: @escaping (AttemptFailure) -> Bool) -> ColdRepeater { + func giveUp(on giveUpCriterium: @escaping @Sendable (AttemptFailure) -> Bool) -> ColdRepeater { let policy = policy.giveUp(on: giveUpCriterium) return ColdRepeater(policy: policy, repeatDelay: repeatDelay, conditionPublisher: conditionPublisher) } @@ -24,7 +24,7 @@ public extension ColdRepeater { return ColdRepeater(policy: policy, repeatDelay: repeatDelay, conditionPublisher: conditionPublisher) } - func giveUpOnErrors(matching finalErrorCriterium: @escaping (Error) -> Bool) -> ColdRepeater { + func giveUpOnErrors(matching finalErrorCriterium: @escaping @Sendable (Error) -> Bool) -> ColdRepeater { let policy = policy.giveUpOnErrors(matching: finalErrorCriterium) return ColdRepeater(policy: policy, repeatDelay: repeatDelay, conditionPublisher: conditionPublisher) } diff --git a/Sources/SwiftRetrier/DSL/ColdRetrier.swift b/Sources/SwiftRetrier/DSL/ColdRetrier.swift index 65d677c..7246fdc 100644 --- a/Sources/SwiftRetrier/DSL/ColdRetrier.swift +++ b/Sources/SwiftRetrier/DSL/ColdRetrier.swift @@ -8,7 +8,7 @@ public struct ColdRetrier { public extension ColdRetrier { - func giveUp(on giveUpCriterium: @escaping (AttemptFailure) -> Bool) -> ColdRetrier { + func giveUp(on giveUpCriterium: @escaping @Sendable (AttemptFailure) -> Bool) -> ColdRetrier { let policy = policy.giveUp(on: giveUpCriterium) return ColdRetrier(policy: policy, conditionPublisher: conditionPublisher) } @@ -23,7 +23,7 @@ public extension ColdRetrier { return ColdRetrier(policy: policy, conditionPublisher: conditionPublisher) } - func giveUpOnErrors(matching finalErrorCriterium: @escaping (Error) -> Bool) -> ColdRetrier { + func giveUpOnErrors(matching finalErrorCriterium: @escaping @Sendable (Error) -> Bool) -> ColdRetrier { let policy = policy.giveUpOnErrors(matching: finalErrorCriterium) return ColdRetrier(policy: policy, conditionPublisher: conditionPublisher) } @@ -40,7 +40,7 @@ public extension ColdRetrier { } @discardableResult - func execute(_ job: @escaping Job) -> AnySingleOutputRetrier { + func execute(_ job: @escaping Job) -> AnySingleOutputRetrier { if let conditionPublisher { return ConditionalRetrier(policy: policy, conditionPublisher: conditionPublisher, job: job) .eraseToAnySingleOutputRetrier() @@ -49,7 +49,7 @@ public extension ColdRetrier { } @discardableResult - func callAsFunction(_ job: @escaping Job) -> AnySingleOutputRetrier { + func callAsFunction(_ job: @escaping Job) -> AnySingleOutputRetrier { execute(job) } } diff --git a/Sources/SwiftRetrier/TypeErasing/AnyRetrier.swift b/Sources/SwiftRetrier/TypeErasing/AnyRetrier.swift index 8c51990..6d934ca 100644 --- a/Sources/SwiftRetrier/TypeErasing/AnyRetrier.swift +++ b/Sources/SwiftRetrier/TypeErasing/AnyRetrier.swift @@ -1,7 +1,7 @@ import Foundation import Combine -public class AnyRetrier: Retrier { +public class AnyRetrier: Retrier, @unchecked Sendable { public let publisherBlock: () -> AnyPublisher, Never> private let cancelBlock: () -> Void diff --git a/Sources/SwiftRetrier/TypeErasing/AnySingleOutputRetrier.swift b/Sources/SwiftRetrier/TypeErasing/AnySingleOutputRetrier.swift index 1e7054a..4bbd7a6 100644 --- a/Sources/SwiftRetrier/TypeErasing/AnySingleOutputRetrier.swift +++ b/Sources/SwiftRetrier/TypeErasing/AnySingleOutputRetrier.swift @@ -1,9 +1,9 @@ import Foundation import Combine -public class AnySingleOutputRetrier: AnyRetrier, SingleOutputRetrier { +public class AnySingleOutputRetrier: AnyRetrier, SingleOutputRetrier, @unchecked Sendable { - private let outputBlock: () async throws -> Output + private let outputBlock: @Sendable () async throws -> Output public init(_ retrier: R) where R: SingleOutputRetrier, R.Output == Value { self.outputBlock = { try await retrier.value } diff --git a/Sources/SwiftRetrier/Util/OnMain.swift b/Sources/SwiftRetrier/Util/OnMain.swift index 31250e0..6abeca8 100644 --- a/Sources/SwiftRetrier/Util/OnMain.swift +++ b/Sources/SwiftRetrier/Util/OnMain.swift @@ -1,6 +1,6 @@ import Foundation -func onMain(_ block: @escaping () -> Void) { +func onMain(_ block: @escaping @Sendable () -> Void) { if Thread.isMainThread { block() } else { diff --git a/Tests/SwiftRetrierTests/Retriers/ConditionalRetrierTests.swift b/Tests/SwiftRetrierTests/Retriers/ConditionalRetrierTests.swift index 09a6dde..b845b3b 100644 --- a/Tests/SwiftRetrierTests/Retriers/ConditionalRetrierTests.swift +++ b/Tests/SwiftRetrierTests/Retriers/ConditionalRetrierTests.swift @@ -82,6 +82,7 @@ class ConditionalRetrierSpecificTests: XCTestCase { try await taskWait() } + @MainActor func test_execution_when_condition_true() { let condition = Just(true) .eraseToAnyPublisher() @@ -123,20 +124,20 @@ class ConditionalRetrierSpecificTests: XCTestCase { cancellable.cancel() } + @MainActor func test_attempt_on_failure_propagated_during_second_trial() { var failedOnce = false var jobExecutionCount = 0 - let job = { + let expectationOwnErrorPropagated = expectation(description: "Attempt own error propagated") + + let retrier = buildRetrier(trueFalseTruePublisher(), { @MainActor in jobExecutionCount += 1 try await taskWait() if !failedOnce { failedOnce = true throw defaultError } - } - let expectationOwnErrorPropagated = expectation(description: "Attempt own error propagated") - - let retrier = buildRetrier(trueFalseTruePublisher(), job) + }) let cancellable = retrier.publisher() .sink(receiveCompletion: { _ in }, receiveValue: { diff --git a/Tests/SwiftRetrierTests/Retriers/ProtocolBasedTests/FallibleRetrierTests.swift b/Tests/SwiftRetrierTests/Retriers/ProtocolBasedTests/FallibleRetrierTests.swift index 6385c30..1d5c6f6 100644 --- a/Tests/SwiftRetrierTests/Retriers/ProtocolBasedTests/FallibleRetrierTests.swift +++ b/Tests/SwiftRetrierTests/Retriers/ProtocolBasedTests/FallibleRetrierTests.swift @@ -20,6 +20,7 @@ class FallibleRetrierTests: XCTestCase { super.tearDown() } + @MainActor func test_Should_PublishCompletionEventWithError_When_JobFailsAndPolicyGivesUp() { let retrier = buildRetrier(Policy.testDefault(maxAttempts: 1), immediateFailureJob) let expectation = expectation(description: "Failure completion received") @@ -34,6 +35,7 @@ class FallibleRetrierTests: XCTestCase { cancellable.cancel() } + @MainActor func test_Should_CompleteSuccessPublisher_When_JobFailsAndPolicyGivesUp() { let retrier = buildRetrier(Policy.testDefault(maxAttempts: 1), immediateFailureJob) let expectation = expectation(description: "Completion received") @@ -46,10 +48,11 @@ class FallibleRetrierTests: XCTestCase { cancellable.cancel() } + @MainActor func test_Should_ThrowErrorInJob_When_RetrierIsCancelled() { let cancellationExpectation = expectation(description: "Cancellation catched") var fulfilled = false - let retrier = buildRetrier(Policy.testDefault(maxAttempts: 1), { + let retrier = buildRetrier(Policy.testDefault(maxAttempts: 1), { @MainActor in do { try await taskWait(defaultJobDuration) } catch { diff --git a/Tests/SwiftRetrierTests/Retriers/ProtocolBasedTests/RetrierTests.swift b/Tests/SwiftRetrierTests/Retriers/ProtocolBasedTests/RetrierTests.swift index 441c9d2..86b3e99 100644 --- a/Tests/SwiftRetrierTests/Retriers/ProtocolBasedTests/RetrierTests.swift +++ b/Tests/SwiftRetrierTests/Retriers/ProtocolBasedTests/RetrierTests.swift @@ -1,5 +1,6 @@ import Foundation import XCTest +import Combine @testable import SwiftRetrier class RetrierTests: XCTestCase { @@ -20,6 +21,7 @@ class RetrierTests: XCTestCase { super.tearDown() } + @MainActor func test_Should_PublishAttemptFailureWithJobError_When_JobFails() { let retrier = buildRetrier({ throw defaultError }) let expectation = expectation(description: "Failure received") @@ -34,6 +36,7 @@ class RetrierTests: XCTestCase { cancellable.cancel() } + @MainActor func test_Should_PublishAttemptSuccess_When_JobSucceeds() { let retrier = buildRetrier(immediateSuccessJob) let expectation = expectation(description: "Success received") @@ -48,9 +51,10 @@ class RetrierTests: XCTestCase { cancellable.cancel() } + @MainActor func test_Should_PublishAttemptFailureThenAttemptSuccess_When_JobFailsThenSucceeds() { var calledOnce = false - let retrier = buildRetrier({ + let retrier = buildRetrier({ @MainActor in if !calledOnce { calledOnce = true throw defaultError @@ -108,6 +112,7 @@ class RetrierTests: XCTestCase { cancellable.cancel() } + @MainActor func test_Should_CompletePublisherWithFinished_When_RetrierIsCancelled() { let failureExpectation = expectation(description: "Failure received") let retrier = buildRetrier(immediateSuccessJob) @@ -133,16 +138,17 @@ class RetrierTests: XCTestCase { @MainActor func test_Should_StillRetry_When_RetrierNotFinishedAndNotRetained() async throws { - var shouldSignalExecution = false + // Just working around capture issue + let shouldSignalExecution = CurrentValueSubject(false) var executed = false - weak var retrier = retrier { - if shouldSignalExecution { + weak var retrier = retrier { @MainActor in + if shouldSignalExecution.value { executed = true } throw defaultError } try await taskWait() - shouldSignalExecution = true + shouldSignalExecution.value = true try await taskWait(defaultSequenceWaitingTime / 2) XCTAssertTrue(executed) retrier?.cancel() diff --git a/Tests/SwiftRetrierTests/Retriers/ProtocolBasedTests/SingleOutputConditionalRetrierTests.swift b/Tests/SwiftRetrierTests/Retriers/ProtocolBasedTests/SingleOutputConditionalRetrierTests.swift index 32e6a0f..2c27ef4 100644 --- a/Tests/SwiftRetrierTests/Retriers/ProtocolBasedTests/SingleOutputConditionalRetrierTests.swift +++ b/Tests/SwiftRetrierTests/Retriers/ProtocolBasedTests/SingleOutputConditionalRetrierTests.swift @@ -41,11 +41,9 @@ class SingleOutputConditionalRetrierTests: XCTestCase { } catch {} } + @MainActor func test_Should_SuccessfullyAwaitValue_When_ConditionPublisherCausesASecondTrialThatSucceeds() { - let job = { - try await taskWait() - } - let retrier = buildRetrier(trueFalseTruePublisher(), job) + let retrier = buildRetrier(trueFalseTruePublisher(), { try await taskWait() }) let expectation = expectation(description: "Receive async output") Task { _ = try await retrier.value @@ -56,12 +54,9 @@ class SingleOutputConditionalRetrierTests: XCTestCase { @MainActor func test_Should_CompletePublisherWithFinished_When_ConditionPublisherCausesASecondTrialThatSucceeds() async { - let job = { - try await taskWait() - } let expectation = expectation(description: "Finished") - let retrier = buildRetrier(trueFalseTruePublisher(), job) + let retrier = buildRetrier(trueFalseTruePublisher(), { try await taskWait() }) let cancellable = retrier.publisher() .sink(receiveCompletion: { if case .finished = $0 { @@ -72,20 +67,20 @@ class SingleOutputConditionalRetrierTests: XCTestCase { cancellable.cancel() } + @MainActor func test_Should_ExecuteJobTheRightNumberOfTimes_When_ConditionPublisherInterruptsATrial() { var failedOnce = false var jobExecutionCount = 0 - let job = { + let completionReceived = expectation(description: "Completion received") + + let retrier = buildRetrier(trueFalseTruePublisher(), { @MainActor in jobExecutionCount += 1 try await taskWait() if !failedOnce { failedOnce = true throw defaultError } - } - let completionReceived = expectation(description: "Completion received") - - let retrier = buildRetrier(trueFalseTruePublisher(), job) + }) let cancellable = retrier.publisher() .sink(receiveCompletion: { _ in completionReceived.fulfill() diff --git a/Tests/SwiftRetrierTests/Retriers/ProtocolBasedTests/SingleOutputFallibleRetrierTests.swift b/Tests/SwiftRetrierTests/Retriers/ProtocolBasedTests/SingleOutputFallibleRetrierTests.swift index a2ab9e1..ebf4fed 100644 --- a/Tests/SwiftRetrierTests/Retriers/ProtocolBasedTests/SingleOutputFallibleRetrierTests.swift +++ b/Tests/SwiftRetrierTests/Retriers/ProtocolBasedTests/SingleOutputFallibleRetrierTests.swift @@ -28,6 +28,7 @@ class SingleOutputFallibleRetrierTests: XCTestCase { } catch {} } + @MainActor func test_Should_ReceiveFinishedCompletionOnFailurePublisher_When_RetrierSucceedsOnLastAttempt() { let retrier = buildRetrier(Policy.testDefault(maxAttempts: 1), immediateSuccessJob) let expectation = expectation(description: "Finished received") @@ -42,6 +43,7 @@ class SingleOutputFallibleRetrierTests: XCTestCase { cancellable.cancel() } + @MainActor func test_Should_ReceiveAttemptFailureOnFailurePublisher_When_JobFailsOnLastAttempt() { let retrier = buildRetrier(Policy.testDefault(maxAttempts: 1), immediateFailureJob) let expectation = expectation(description: "Attempt failure received") diff --git a/Tests/SwiftRetrierTests/Retriers/ProtocolBasedTests/SingleOutputRetrierTests.swift b/Tests/SwiftRetrierTests/Retriers/ProtocolBasedTests/SingleOutputRetrierTests.swift index cb406ce..45d2aba 100644 --- a/Tests/SwiftRetrierTests/Retriers/ProtocolBasedTests/SingleOutputRetrierTests.swift +++ b/Tests/SwiftRetrierTests/Retriers/ProtocolBasedTests/SingleOutputRetrierTests.swift @@ -19,6 +19,7 @@ class SingleOutputRetrierTests: XCTestCase { super.tearDown() } + @MainActor func test_Should_CompletePublisherWithFinished_When_JobSucceeds() { let retrier = buildRetrier(immediateSuccessJob) let expectation = expectation(description: "Finished received") @@ -33,6 +34,7 @@ class SingleOutputRetrierTests: XCTestCase { cancellable.cancel() } + @MainActor func test_Should_CompleteSuccessPublisherWithFinished_When_JobSucceeds() { let retrier = buildRetrier(immediateSuccessJob) let expectation = expectation(description: "Finished received") @@ -47,9 +49,10 @@ class SingleOutputRetrierTests: XCTestCase { cancellable.cancel() } + @MainActor func test_Should_CompletePublisherWithFinished_When_JobSucceedsAfterOneRetry() { var calledOnce = false - let retrier = buildRetrier({ + let retrier = buildRetrier({ @MainActor in if !calledOnce { calledOnce = true throw defaultError @@ -66,9 +69,10 @@ class SingleOutputRetrierTests: XCTestCase { cancellable.cancel() } + @MainActor func test_Should_ReceiveAsyncValue_When_JobSucceedsAfterOneRetry() { var calledOnce = false - let retrier = buildRetrier({ + let retrier = buildRetrier({ @MainActor in if !calledOnce { calledOnce = true throw defaultError diff --git a/Tests/SwiftRetrierTests/Retriers/RepeaterTests.swift b/Tests/SwiftRetrierTests/Retriers/RepeaterTests.swift index 4c77e45..394f236 100644 --- a/Tests/SwiftRetrierTests/Retriers/RepeaterTests.swift +++ b/Tests/SwiftRetrierTests/Retriers/RepeaterTests.swift @@ -36,10 +36,11 @@ class RepeaterTests: XCTestCase { super.tearDown() } + @MainActor func test_repeats() { var count = 0 let expectation = expectation(description: "Should repeat") - _ = buildRetrier(repeatDelay, { + _ = buildRetrier(repeatDelay, { @MainActor in count += 1 if count == 3 { expectation.fulfill()