From a3e95eff312dfd0fe32a5c42776af15851e5facd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20S=C3=A1nchez-Dehesa?= Date: Sat, 2 Oct 2021 22:48:36 +0200 Subject: [PATCH] Change indentation --- Package.swift | 30 +- sources/runtime/operators/AssignOp.swift | 176 +++---- sources/runtime/operators/AwaitOp.swift | 42 +- sources/runtime/operators/HandleEndOp.swift | 16 +- sources/runtime/operators/ResultOp.swift | 62 +-- sources/runtime/operators/RetryOp.swift | 18 +- sources/runtime/operators/SinkOp.swift | 46 +- sources/runtime/operators/ThenOp.swift | 16 +- .../runtime/publishers/DeferredComplete.swift | 126 ++--- .../runtime/publishers/DeferredFuture.swift | 150 +++--- .../publishers/DeferredPassthrough.swift | 172 +++--- .../runtime/publishers/DeferredResult.swift | 108 ++-- .../publishers/DeferredTryComplete.swift | 118 ++--- .../runtime/publishers/DeferredTryValue.swift | 118 ++--- .../runtime/publishers/DeferredValue.swift | 104 ++-- sources/runtime/publishers/HandleEnd.swift | 202 ++++---- sources/runtime/publishers/Retry.swift | 490 +++++++++--------- sources/runtime/publishers/Then.swift | 476 ++++++++--------- sources/runtime/subscribers/FixedSink.swift | 172 +++--- .../runtime/subscribers/GraduatedSink.swift | 116 ++--- sources/runtime/utils/Buffer.swift | 18 +- sources/runtime/utils/Lock.swift | 134 ++--- sources/runtime/utils/State.swift | 84 +-- sources/testing/Expectations.swift | 334 ++++++------ sources/testing/XCTestCase.swift | 26 +- tests/runtime/operators/AssignOpTests.swift | 92 ++-- tests/runtime/operators/AwaitOpTests.swift | 36 +- .../operators/DelayedRetryOpTests.swift | 184 +++---- .../runtime/operators/HandleEndOpTests.swift | 246 ++++----- tests/runtime/operators/ResultOpTests.swift | 133 ++--- tests/runtime/operators/ThenOpTests.swift | 92 ++-- .../publishers/DeferredCompleteTests.swift | 162 +++--- .../publishers/DeferredFutureTests.swift | 142 ++--- .../publishers/DeferredPassthroughTests.swift | 156 +++--- .../publishers/DeferredResultTests.swift | 88 ++-- .../publishers/DeferredTryCompleteTests.swift | 148 +++--- .../publishers/DeferredTryValueTests.swift | 84 +-- .../publishers/DeferredValueTests.swift | 51 +- .../runtime/subscribers/FixedSinkTests.swift | 149 +++--- .../subscribers/GraduatedSinkTests.swift | 105 ++-- tests/runtime/utils/BufferTests.swift | 126 ++--- tests/testing/ExpectationsTests.swift | 146 +++--- 42 files changed, 2751 insertions(+), 2743 deletions(-) diff --git a/Package.swift b/Package.swift index ab87817..3ce75a7 100644 --- a/Package.swift +++ b/Package.swift @@ -2,19 +2,19 @@ import PackageDescription var package = Package( - name: "Conbini", - platforms: [ - .macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6) - ], - products: [ - .library(name: "Conbini", targets: ["Conbini"]), - .library(name: "ConbiniForTesting", targets: ["ConbiniForTesting"]) - ], - dependencies: [], - targets: [ - .target(name: "Conbini", path: "sources/runtime"), - .target(name: "ConbiniForTesting", path: "sources/testing"), - .testTarget(name: "ConbiniTests", dependencies: ["Conbini"], path: "tests/runtime"), - .testTarget(name: "ConbiniForTestingTests", dependencies: ["Conbini", "ConbiniForTesting"], path: "tests/testing"), - ] + name: "Conbini", + platforms: [ + .macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6) + ], + products: [ + .library(name: "Conbini", targets: ["Conbini"]), + .library(name: "ConbiniForTesting", targets: ["ConbiniForTesting"]) + ], + dependencies: [], + targets: [ + .target(name: "Conbini", path: "sources/runtime"), + .target(name: "ConbiniForTesting", path: "sources/testing"), + .testTarget(name: "ConbiniTests", dependencies: ["Conbini"], path: "tests/runtime"), + .testTarget(name: "ConbiniForTestingTests", dependencies: ["Conbini", "ConbiniForTesting"], path: "tests/testing"), + ] ) diff --git a/sources/runtime/operators/AssignOp.swift b/sources/runtime/operators/AssignOp.swift index b53ac4d..4e534cd 100644 --- a/sources/runtime/operators/AssignOp.swift +++ b/sources/runtime/operators/AssignOp.swift @@ -1,97 +1,97 @@ import Combine extension Publisher where Self.Failure==Never { - /// Assigns a publisher's output to a property of an object. - /// - /// The difference between `assign(to:onWeak:)` and Combine's `assign(to:on:)` is two-fold: - /// - `assign(to:onWeak:)` doesn't set a _strong bond_ to `object`. - /// This breaks memory cycles when `object` also stores the returned cancellable (e.g. passing `self` is a common case). - /// - `assign(to:onWeak:)` cancels the upstream publisher if it detects `object` is deinitialized. - /// - /// The difference between is that a _strong bond_ is not set to the`object`. This breaks memory cycles when `object` also stores the returned cancellable (e.g. passing `self` is a common case). - /// - parameter keyPath: A key path that indicates the property to assign. - /// - parameter object: The object that contains the property. The subscriber assigns the object's property every time it receives a new value. - /// - returns: An `AnyCancellable` instance. Call `cancel()` on the instance when you no longer want the publisher to automatically assign the property. Deinitializing this instance will also cancel automatic assignment. - @_transparent public func assign(to keyPath: ReferenceWritableKeyPath, onWeak object: Root) -> AnyCancellable where Root:AnyObject { - weak var cancellable: AnyCancellable? = nil - let cleanup: (Subscribers.Completion) -> Void = { _ in - cancellable?.cancel() - cancellable = nil - } - - let subscriber = Subscribers.Sink(receiveCompletion: cleanup, receiveValue: { [weak object] (value) in - guard let object = object else { return cleanup(.finished) } - object[keyPath: keyPath] = value - }) - - let result = AnyCancellable(subscriber) - cancellable = result - self.subscribe(subscriber) - return result - } - - /// Assigns a publisher's output to a property of an object. - /// - /// The difference between `assign(to:onUnowned:)` and Combine's `assign(to:on:)` is that a _strong bond_ is not set to the`object`. This breaks memory cycles when `object` also stores the returned cancellable (e.g. passing `self` is a common case). - /// - parameter keyPath: A key path that indicates the property to assign. - /// - parameter object: The object that contains the property. The subscriber assigns the object's property every time it receives a new value. - /// - returns: An `AnyCancellable` instance. Call `cancel()` on the instance when you no longer want the publisher to automatically assign the property. Deinitializing this instance will also cancel automatic assignment. - @_transparent public func assign(to keyPath: ReferenceWritableKeyPath, onUnowned object: Root) -> AnyCancellable where Root:AnyObject { - self.sink(receiveValue: { [unowned object] (value) in - object[keyPath: keyPath] = value - }) + /// Assigns a publisher's output to a property of an object. + /// + /// The difference between `assign(to:onWeak:)` and Combine's `assign(to:on:)` is two-fold: + /// - `assign(to:onWeak:)` doesn't set a _strong bond_ to `object`. + /// This breaks memory cycles when `object` also stores the returned cancellable (e.g. passing `self` is a common case). + /// - `assign(to:onWeak:)` cancels the upstream publisher if it detects `object` is deinitialized. + /// + /// The difference between is that a _strong bond_ is not set to the`object`. This breaks memory cycles when `object` also stores the returned cancellable (e.g. passing `self` is a common case). + /// - parameter keyPath: A key path that indicates the property to assign. + /// - parameter object: The object that contains the property. The subscriber assigns the object's property every time it receives a new value. + /// - returns: An `AnyCancellable` instance. Call `cancel()` on the instance when you no longer want the publisher to automatically assign the property. Deinitializing this instance will also cancel automatic assignment. + @_transparent public func assign(to keyPath: ReferenceWritableKeyPath, onWeak object: Root) -> AnyCancellable where Root:AnyObject { + weak var cancellable: AnyCancellable? = nil + let cleanup: (Subscribers.Completion) -> Void = { _ in + cancellable?.cancel() + cancellable = nil } + + let subscriber = Subscribers.Sink(receiveCompletion: cleanup, receiveValue: { [weak object] (value) in + guard let object = object else { return cleanup(.finished) } + object[keyPath: keyPath] = value + }) + + let result = AnyCancellable(subscriber) + cancellable = result + self.subscribe(subscriber) + return result + } + + /// Assigns a publisher's output to a property of an object. + /// + /// The difference between `assign(to:onUnowned:)` and Combine's `assign(to:on:)` is that a _strong bond_ is not set to the`object`. This breaks memory cycles when `object` also stores the returned cancellable (e.g. passing `self` is a common case). + /// - parameter keyPath: A key path that indicates the property to assign. + /// - parameter object: The object that contains the property. The subscriber assigns the object's property every time it receives a new value. + /// - returns: An `AnyCancellable` instance. Call `cancel()` on the instance when you no longer want the publisher to automatically assign the property. Deinitializing this instance will also cancel automatic assignment. + @_transparent public func assign(to keyPath: ReferenceWritableKeyPath, onUnowned object: Root) -> AnyCancellable where Root:AnyObject { + self.sink(receiveValue: { [unowned object] (value) in + object[keyPath: keyPath] = value + }) + } } extension Publisher where Self.Failure==Never { - /// Invoke on the given instance the specified method. - /// - remark: A strong bond is set to `instance`. If you store the cancellable in the same instance as `instance`, a memory cycle will be created. - /// - parameter method: A method/function metatype. - /// - parameter instance: The instance defining the specified method. - /// - returns: An `AnyCancellable` instance. Call `cancel()` on the instance when you no longer want the publisher to automatically call the method. Deinitializing this instance will also cancel automatic invocation. - @_transparent public func invoke(_ method: @escaping (Root)->(Output)->Void, on instance: Root) -> AnyCancellable { - return self.sink(receiveValue: { (value) in - method(instance)(value) - }) - } - - /// Invoke on the given instance the specified method. - /// - /// The difference between `invoke(_:onWeak:)` and Combine's `invoke(_:on:)` is two-fold: - /// - `invoke(_:onWeak:)` doesn't set a _strong bond_ to `object`. - /// This breaks memory cycles when `object` also stores the returned cancellable (e.g. passing `self` is a common case). - /// - `invoke(_:onWeak:)` cancels the upstream publisher if it detects `object` is deinitialized. - /// - /// - parameter method: A method/function metatype. - /// - parameter instance: The instance defining the specified method. - /// - returns: An `AnyCancellable` instance. Call `cancel()` on the instance when you no longer want the publisher to automatically call the method. Deinitializing this instance will also cancel automatic invocation. - @_transparent public func invoke(_ method: @escaping (Root)->(Output)->Void, onWeak object: Root) -> AnyCancellable where Root:AnyObject { - weak var cancellable: AnyCancellable? = nil - let cleanup: (Subscribers.Completion) -> Void = { _ in - cancellable?.cancel() - cancellable = nil - } - - let subscriber = Subscribers.Sink(receiveCompletion: cleanup, receiveValue: { [weak object] (value) in - guard let object = object else { return cleanup(.finished) } - method(object)(value) - }) - - let result = AnyCancellable(subscriber) - cancellable = result - self.subscribe(subscriber) - return result - } - - /// Invoke on the given instance the specified method. - /// - /// The difference between `invoke(_:onUnowned:)` and Combine's `invoke(_:on:)` is that a _strong bond_ is not set to the`object`. This breaks memory cycles when `object` also stores the returned cancellable (e.g. passing `self` is a common case). - /// - parameter method: A method/function metatype. - /// - parameter instance: The instance defining the specified method. - /// - returns: An `AnyCancellable` instance. Call `cancel()` on the instance when you no longer want the publisher to automatically call the method. Deinitializing this instance will also cancel automatic invocation. - @_transparent public func invoke(_ method: @escaping (Root)->(Output)->Void, onUnowned object: Root) -> AnyCancellable where Root:AnyObject { - return self.sink(receiveValue: { [unowned object] (value) in - method(object)(value) - }) + /// Invoke on the given instance the specified method. + /// - remark: A strong bond is set to `instance`. If you store the cancellable in the same instance as `instance`, a memory cycle will be created. + /// - parameter method: A method/function metatype. + /// - parameter instance: The instance defining the specified method. + /// - returns: An `AnyCancellable` instance. Call `cancel()` on the instance when you no longer want the publisher to automatically call the method. Deinitializing this instance will also cancel automatic invocation. + @_transparent public func invoke(_ method: @escaping (Root)->(Output)->Void, on instance: Root) -> AnyCancellable { + self.sink(receiveValue: { (value) in + method(instance)(value) + }) + } + + /// Invoke on the given instance the specified method. + /// + /// The difference between `invoke(_:onWeak:)` and Combine's `invoke(_:on:)` is two-fold: + /// - `invoke(_:onWeak:)` doesn't set a _strong bond_ to `object`. + /// This breaks memory cycles when `object` also stores the returned cancellable (e.g. passing `self` is a common case). + /// - `invoke(_:onWeak:)` cancels the upstream publisher if it detects `object` is deinitialized. + /// + /// - parameter method: A method/function metatype. + /// - parameter instance: The instance defining the specified method. + /// - returns: An `AnyCancellable` instance. Call `cancel()` on the instance when you no longer want the publisher to automatically call the method. Deinitializing this instance will also cancel automatic invocation. + @_transparent public func invoke(_ method: @escaping (Root)->(Output)->Void, onWeak object: Root) -> AnyCancellable where Root:AnyObject { + weak var cancellable: AnyCancellable? = nil + let cleanup: (Subscribers.Completion) -> Void = { _ in + cancellable?.cancel() + cancellable = nil } + + let subscriber = Subscribers.Sink(receiveCompletion: cleanup, receiveValue: { [weak object] (value) in + guard let object = object else { return cleanup(.finished) } + method(object)(value) + }) + + let result = AnyCancellable(subscriber) + cancellable = result + self.subscribe(subscriber) + return result + } + + /// Invoke on the given instance the specified method. + /// + /// The difference between `invoke(_:onUnowned:)` and Combine's `invoke(_:on:)` is that a _strong bond_ is not set to the`object`. This breaks memory cycles when `object` also stores the returned cancellable (e.g. passing `self` is a common case). + /// - parameter method: A method/function metatype. + /// - parameter instance: The instance defining the specified method. + /// - returns: An `AnyCancellable` instance. Call `cancel()` on the instance when you no longer want the publisher to automatically call the method. Deinitializing this instance will also cancel automatic invocation. + @_transparent public func invoke(_ method: @escaping (Root)->(Output)->Void, onUnowned object: Root) -> AnyCancellable where Root:AnyObject { + self.sink(receiveValue: { [unowned object] (value) in + method(object)(value) + }) + } } diff --git a/sources/runtime/operators/AwaitOp.swift b/sources/runtime/operators/AwaitOp.swift index f409102..50a28a1 100644 --- a/sources/runtime/operators/AwaitOp.swift +++ b/sources/runtime/operators/AwaitOp.swift @@ -2,26 +2,26 @@ import Combine import Foundation extension Publisher { - /// Subscribes to the receiving publichser and expects a single value and a subsequent successfull completion. - /// - /// If no values is received, or more than one value is received, or a failure is received, the program will crash. - /// - warning: The publisher must receive the value and completion event in a different queue from the queue where this property is called or the code will never execute. - @inlinable public var await: Output { - let group = DispatchGroup() - group.enter() - - var value: Output? = nil - let cancellable = self.sink(fixedDemand: 1, receiveCompletion: { - switch $0 { - case .failure(let error): fatalError("\(error)") - case .finished: - guard case .some = value else { fatalError() } - group.leave() - } - }, receiveValue: { value = $0 }) + /// Subscribes to the receiving publichser and expects a single value and a subsequent successfull completion. + /// + /// If no values is received, or more than one value is received, or a failure is received, the program will crash. + /// - warning: The publisher must receive the value and completion event in a different queue from the queue where this property is called or the code will never execute. + @inlinable public var await: Output { + let group = DispatchGroup() + group.enter() - group.wait() - cancellable.cancel() - return value! - } + var value: Output? = nil + let cancellable = self.sink(fixedDemand: 1, receiveCompletion: { + switch $0 { + case .failure(let error): fatalError("\(error)") + case .finished: + guard case .some = value else { fatalError() } + group.leave() + } + }, receiveValue: { value = $0 }) + + group.wait() + cancellable.cancel() + return value! + } } diff --git a/sources/runtime/operators/HandleEndOp.swift b/sources/runtime/operators/HandleEndOp.swift index 83f95bc..83e7ac5 100644 --- a/sources/runtime/operators/HandleEndOp.swift +++ b/sources/runtime/operators/HandleEndOp.swift @@ -1,12 +1,12 @@ import Combine extension Publisher { - /// Performs the specified closure when the publisher completes (whether successfully or with a failure) or when the publisher gets cancelled. - /// - /// The closure will get executed exactly once. - /// - parameter handle: A closure that executes when the publisher receives a completion event or when the publisher gets cancelled. - /// - parameter completion: A completion event if the publisher completes (whether successfully or not), or `nil` in case the publisher is cancelled. - @inlinable public func handleEnd(_ handle: @escaping (_ completion: Subscribers.Completion?)->Void) -> Publishers.HandleEnd { - .init(upstream: self, handle: handle) - } + /// Performs the specified closure when the publisher completes (whether successfully or with a failure) or when the publisher gets cancelled. + /// + /// The closure will get executed exactly once. + /// - parameter handle: A closure that executes when the publisher receives a completion event or when the publisher gets cancelled. + /// - parameter completion: A completion event if the publisher completes (whether successfully or not), or `nil` in case the publisher is cancelled. + @inlinable public func handleEnd(_ handle: @escaping (_ completion: Subscribers.Completion?)->Void) -> Publishers.HandleEnd { + .init(upstream: self, handle: handle) + } } diff --git a/sources/runtime/operators/ResultOp.swift b/sources/runtime/operators/ResultOp.swift index 1d38e34..139b02d 100644 --- a/sources/runtime/operators/ResultOp.swift +++ b/sources/runtime/operators/ResultOp.swift @@ -1,35 +1,35 @@ import Combine extension Publisher { - /// Subscribes to the upstream and expects a single value and a subsequent successful completion. - /// - /// The underlying subscriber is `Subscriber.FixedSink`, which makes it impossible to receive more than one value. That said, here are the possible scenarios: - /// - If a single value is published, the handler is called with such value. - /// - If a failure occurs; the handler is called with such failure. - /// - If a completion occurs and no value has been sent, the subscriber gets cancelled, `onEmpty` is called, and depending on whether an error is generated, the handler is called or not. - /// - parameter onEmpty: Autoclosure generating an optional error to pass to the `handler` when upstream doesn't behave as expected. If `nil`, the `handler` won't be called when no values are published. - /// - parameter handler: Returns the result of the publisher. - /// - parameter result: The value yielded after the subscription. - /// - returns: `Cancellable` able to stop/cancel the subscription. - @inlinable @discardableResult public func result(onEmpty: @escaping @autoclosure ()->Failure? = nil, _ handler: @escaping (_ result: Result)->Void) -> AnyCancellable { - var value: Output? = nil - - let subscriber = Subscribers.FixedSink(demand: 1, receiveCompletion: { - switch $0 { - case .failure(let error): - handler(.failure(error)) - case .finished: - if let value = value { - handler(.success(value)) - } else if let error = onEmpty() { - handler(.failure(error)) - } - } - - value = nil - }, receiveValue: { value = $0 }) - - self.subscribe(subscriber) - return AnyCancellable(subscriber) - } + /// Subscribes to the upstream and expects a single value and a subsequent successful completion. + /// + /// The underlying subscriber is `Subscriber.FixedSink`, which makes it impossible to receive more than one value. That said, here are the possible scenarios: + /// - If a single value is published, the handler is called with such value. + /// - If a failure occurs; the handler is called with such failure. + /// - If a completion occurs and no value has been sent, the subscriber gets cancelled, `onEmpty` is called, and depending on whether an error is generated, the handler is called or not. + /// - parameter onEmpty: Autoclosure generating an optional error to pass to the `handler` when upstream doesn't behave as expected. If `nil`, the `handler` won't be called when no values are published. + /// - parameter handler: Returns the result of the publisher. + /// - parameter result: The value yielded after the subscription. + /// - returns: `Cancellable` able to stop/cancel the subscription. + @inlinable @discardableResult public func result(onEmpty: @escaping @autoclosure ()->Failure? = nil, _ handler: @escaping (_ result: Result)->Void) -> AnyCancellable { + var value: Output? = nil + + let subscriber = Subscribers.FixedSink(demand: 1, receiveCompletion: { + switch $0 { + case .failure(let error): + handler(.failure(error)) + case .finished: + if let value = value { + handler(.success(value)) + } else if let error = onEmpty() { + handler(.failure(error)) + } + } + + value = nil + }, receiveValue: { value = $0 }) + + self.subscribe(subscriber) + return AnyCancellable(subscriber) + } } diff --git a/sources/runtime/operators/RetryOp.swift b/sources/runtime/operators/RetryOp.swift index 13f1d79..02bf9fe 100644 --- a/sources/runtime/operators/RetryOp.swift +++ b/sources/runtime/operators/RetryOp.swift @@ -2,13 +2,13 @@ import Combine import Foundation extension Publisher { - /// Attempts to recreate a failed subscription with the upstream publisher using a specified number of attempts to establish the connection and a given amount of seconds between attempts. - /// - parameter scheduler: The scheduler used to wait for the specific intervals. - /// - parameter tolerance: The tolerance used when scheduling a new attempt after a failure. A default implies the minimum tolerance. - /// - parameter options: The options for the given scheduler. - /// - parameter intervals: The amount of seconds to wait after a failure occurrence. Negative values are considered zero. - /// - returns: A publisher that attemps to recreate its subscription to a failed upstream publisher a given amount of times and waiting a given amount of seconds between attemps. - @inlinable public func retry(on scheduler: S, tolerance: S.SchedulerTimeType.Stride? = nil, options: S.SchedulerOptions? = nil, intervals: [TimeInterval]) -> Publishers.DelayedRetry where S:Scheduler { - .init(upstream: self, scheduler: scheduler, options: options, intervals: intervals) - } + /// Attempts to recreate a failed subscription with the upstream publisher using a specified number of attempts to establish the connection and a given amount of seconds between attempts. + /// - parameter scheduler: The scheduler used to wait for the specific intervals. + /// - parameter tolerance: The tolerance used when scheduling a new attempt after a failure. A default implies the minimum tolerance. + /// - parameter options: The options for the given scheduler. + /// - parameter intervals: The amount of seconds to wait after a failure occurrence. Negative values are considered zero. + /// - returns: A publisher that attemps to recreate its subscription to a failed upstream publisher a given amount of times and waiting a given amount of seconds between attemps. + @inlinable public func retry(on scheduler: S, tolerance: S.SchedulerTimeType.Stride? = nil, options: S.SchedulerOptions? = nil, intervals: [TimeInterval]) -> Publishers.DelayedRetry where S:Scheduler { + .init(upstream: self, scheduler: scheduler, options: options, intervals: intervals) + } } diff --git a/sources/runtime/operators/SinkOp.swift b/sources/runtime/operators/SinkOp.swift index 298b764..610a442 100644 --- a/sources/runtime/operators/SinkOp.swift +++ b/sources/runtime/operators/SinkOp.swift @@ -1,27 +1,27 @@ import Combine extension Publisher { - /// Subscribe to the receiving publisher and request exactly `fixedDemand` values. - /// - /// This operator may receive zero to `fixedDemand` of values before completing, but no more. - /// - parameter fixedDemand: The maximum number of values to be received. - /// - parameter receiveCompletion: The closure executed when the provided amount of values are received or a completion event is received. - /// - parameter receiveValue: The closure executed when a value is received. - @inlinable public func sink(fixedDemand: Int, receiveCompletion: ((Subscribers.Completion)->Void)?, receiveValue: ((Output)->Void)?) -> AnyCancellable { - let subscriber = Subscribers.FixedSink(demand: fixedDemand, receiveCompletion: receiveCompletion, receiveValue: receiveValue) - let cancellable = AnyCancellable(subscriber) - self.subscribe(subscriber) - return cancellable - } - - /// Subscribe to the receiving publisher requesting `maxDemand` values and always keeping the same backpressure. - /// - parameter maxDemand: The maximum number of in-flight values. - /// - parameter receiveCompletion: The closure executed when the provided amount of values are received or a completion event is received. - /// - parameter receiveValue: The closure executed when a value is received. - @inlinable public func sink(maxDemand: Subscribers.Demand, receiveCompletion: ((Subscribers.Completion)->Void)?, receiveValue: ((Output)->Void)?) -> AnyCancellable { - let subscriber = Subscribers.GraduatedSink(maxDemand: maxDemand, receiveCompletion: receiveCompletion, receiveValue: receiveValue) - let cancellable = AnyCancellable(subscriber) - self.subscribe(subscriber) - return cancellable - } + /// Subscribe to the receiving publisher and request exactly `fixedDemand` values. + /// + /// This operator may receive zero to `fixedDemand` of values before completing, but no more. + /// - parameter fixedDemand: The maximum number of values to be received. + /// - parameter receiveCompletion: The closure executed when the provided amount of values are received or a completion event is received. + /// - parameter receiveValue: The closure executed when a value is received. + @inlinable public func sink(fixedDemand: Int, receiveCompletion: ((Subscribers.Completion)->Void)?, receiveValue: ((Output)->Void)?) -> AnyCancellable { + let subscriber = Subscribers.FixedSink(demand: fixedDemand, receiveCompletion: receiveCompletion, receiveValue: receiveValue) + let cancellable = AnyCancellable(subscriber) + self.subscribe(subscriber) + return cancellable + } + + /// Subscribe to the receiving publisher requesting `maxDemand` values and always keeping the same backpressure. + /// - parameter maxDemand: The maximum number of in-flight values. + /// - parameter receiveCompletion: The closure executed when the provided amount of values are received or a completion event is received. + /// - parameter receiveValue: The closure executed when a value is received. + @inlinable public func sink(maxDemand: Subscribers.Demand, receiveCompletion: ((Subscribers.Completion)->Void)?, receiveValue: ((Output)->Void)?) -> AnyCancellable { + let subscriber = Subscribers.GraduatedSink(maxDemand: maxDemand, receiveCompletion: receiveCompletion, receiveValue: receiveValue) + let cancellable = AnyCancellable(subscriber) + self.subscribe(subscriber) + return cancellable + } } diff --git a/sources/runtime/operators/ThenOp.swift b/sources/runtime/operators/ThenOp.swift index e4fc965..506b7c7 100644 --- a/sources/runtime/operators/ThenOp.swift +++ b/sources/runtime/operators/ThenOp.swift @@ -1,12 +1,12 @@ import Combine extension Publisher { - /// Ignores all upstream value events and when it completes successfully, the operator switches to the provided publisher. - /// - /// The `transform` closure will only be executed once a successful completion event arrives. If a completion event doesn't arrive or the completion is a failure, the closure is never executed. - /// - parameter maxDemand: The maximum demand requested to the upstream at the same time. For example, if `.max(5)` is requested, a demand of 5 is kept until the upstream completes. - /// - parameter transform: Closure generating the stream to be switched to once a successful completion event is received from upstream. - @inlinable public func then(maxDemand: Subscribers.Demand = .unlimited, _ transform: @escaping ()->Child) -> Publishers.Then where Child:Publisher, Self.Failure==Child.Failure { - Publishers.Then(upstream: self, maxDemand: maxDemand, transform: transform) - } + /// Ignores all upstream value events and when it completes successfully, the operator switches to the provided publisher. + /// + /// The `transform` closure will only be executed once a successful completion event arrives. If a completion event doesn't arrive or the completion is a failure, the closure is never executed. + /// - parameter maxDemand: The maximum demand requested to the upstream at the same time. For example, if `.max(5)` is requested, a demand of 5 is kept until the upstream completes. + /// - parameter transform: Closure generating the stream to be switched to once a successful completion event is received from upstream. + @inlinable public func then(maxDemand: Subscribers.Demand = .unlimited, _ transform: @escaping ()->Child) -> Publishers.Then where Child:Publisher, Self.Failure==Child.Failure { + Publishers.Then(upstream: self, maxDemand: maxDemand, transform: transform) + } } diff --git a/sources/runtime/publishers/DeferredComplete.swift b/sources/runtime/publishers/DeferredComplete.swift index a93c113..e1823d8 100644 --- a/sources/runtime/publishers/DeferredComplete.swift +++ b/sources/runtime/publishers/DeferredComplete.swift @@ -4,74 +4,74 @@ import Combine /// /// This publisher only executes the stored closure when it receives a request with a demand greater than zero. Right after closure execution, the closure is removed and cleaned up. public struct DeferredComplete: Publisher where Failure:Swift.Error { - /// The closure type being store for delayed execution. - public typealias Closure = () -> Failure? - - /// Deferred closure. - /// - attention: The closure is kept till a greater-than-zero demand is received (at which point, it is executed and then deleted). - public let closure: Closure - - /// Creates a publisher that forwards a successful completion once it receives a positive request (i.e. a request greater than zero) - public init() { - self.closure = { nil } - } - - /// Creates a publisher that completes successfully or fails depending on the result of the given closure. - /// - parameter output: The output type of this *empty* publisher. It is given here as convenience, since it may help compiler inferral. - /// - parameter closure: The closure which produces an empty successful completion (if it returns `nil`) or a failure (if it returns an error). - /// - attention: The closure is kept till a greater-than-zero demand is received (at which point, it is executed and then deleted). - @inlinable public init(output: Output.Type = Output.self, closure: @escaping Closure) { - self.closure = closure + /// The closure type being store for delayed execution. + public typealias Closure = () -> Failure? + + /// Deferred closure. + /// - attention: The closure is kept till a greater-than-zero demand is received (at which point, it is executed and then deleted). + public let closure: Closure + + /// Creates a publisher that forwards a successful completion once it receives a positive request (i.e. a request greater than zero) + public init() { + self.closure = { nil } + } + + /// Creates a publisher that completes successfully or fails depending on the result of the given closure. + /// - parameter output: The output type of this *empty* publisher. It is given here as convenience, since it may help compiler inferral. + /// - parameter closure: The closure which produces an empty successful completion (if it returns `nil`) or a failure (if it returns an error). + /// - attention: The closure is kept till a greater-than-zero demand is received (at which point, it is executed and then deleted). + @inlinable public init(output: Output.Type = Output.self, closure: @escaping Closure) { + self.closure = closure + } + + /// Creates a publisher that fails with the error provided. + /// - parameter error: *Autoclosure* that will get executed on the first positive request (i.e. a request greater than zero). + @inlinable public init(error: @autoclosure @escaping ()->Failure) { + self.closure = { error() } + } + + public func receive(subscriber: S) where S:Subscriber, S.Input==Output, S.Failure==Failure { + let subscription = _Conduit(downstream: subscriber, closure: self.closure) + subscriber.receive(subscription: subscription) + } +} + +private extension DeferredComplete { + /// The shadow subscription chain's origin. + final class _Conduit: Subscription where Downstream:Subscriber, Downstream.Failure==Failure { + /// Enum listing all possible conduit states. + @ConduitLock private var state: ConduitState + + init(downstream: Downstream, closure: @escaping Closure) { + self.state = .active(_Configuration(downstream: downstream, closure: closure)) } - - /// Creates a publisher that fails with the error provided. - /// - parameter error: *Autoclosure* that will get executed on the first positive request (i.e. a request greater than zero). - @inlinable public init(error: @autoclosure @escaping ()->Failure) { - self.closure = { error() } + + deinit { + self._state.invalidate() } - - public func receive(subscriber: S) where S:Subscriber, S.Input==Output, S.Failure==Failure { - let subscription = Conduit(downstream: subscriber, closure: self.closure) - subscriber.receive(subscription: subscription) + + func request(_ demand: Subscribers.Demand) { + guard demand > 0, case .active(let config) = self._state.terminate() else { return } + + if let error = config.closure() { + return config.downstream.receive(completion: .failure(error)) + } else { + return config.downstream.receive(completion: .finished) + } } -} -fileprivate extension DeferredComplete { - /// The shadow subscription chain's origin. - final class Conduit: Subscription where Downstream:Subscriber, Downstream.Failure==Failure { - /// Enum listing all possible conduit states. - @ConduitLock private var state: ConduitState - - init(downstream: Downstream, closure: @escaping Closure) { - self.state = .active(_Configuration(downstream: downstream, closure: closure)) - } - - deinit { - self._state.invalidate() - } - - func request(_ demand: Subscribers.Demand) { - guard demand > 0, case .active(let config) = self._state.terminate() else { return } - - if let error = config.closure() { - return config.downstream.receive(completion: .failure(error)) - } else { - return config.downstream.receive(completion: .finished) - } - } - - func cancel() { - self._state.terminate() - } + func cancel() { + self._state.terminate() } + } } -private extension DeferredComplete.Conduit { - /// Values needed for the subscription's active state. - struct _Configuration { - /// The downstream subscriber awaiting any value and/or completion events. - let downstream: Downstream - /// The closure generating the succesful/failure completion. - let closure: DeferredComplete.Closure - } +private extension DeferredComplete._Conduit { + /// Values needed for the subscription's active state. + struct _Configuration { + /// The downstream subscriber awaiting any value and/or completion events. + let downstream: Downstream + /// The closure generating the succesful/failure completion. + let closure: DeferredComplete.Closure + } } diff --git a/sources/runtime/publishers/DeferredFuture.swift b/sources/runtime/publishers/DeferredFuture.swift index 74b25f6..77a402d 100644 --- a/sources/runtime/publishers/DeferredFuture.swift +++ b/sources/runtime/publishers/DeferredFuture.swift @@ -4,87 +4,87 @@ import Combine /// /// This publisher only executes the stored closure when it receives a request with a demand greater than zero. Right after the closure execution, the closure is removed and clean up. public struct DeferredFuture: Publisher { - /// The promise returning the value (or failure) of the whole publisher. - public typealias Promise = (Result) -> Void - /// The closure type being store for delayed execution. - public typealias Closure = (_ promise: @escaping Promise) -> Void - - /// Deferred closure. - /// - attention: The closure is kept till a greater-than-zero demand is received (at which point, it is executed and then deleted). - public let closure: Closure - - /// Creates a publisher that send a value and completes successfully or just fails depending on the result of the given closure. - /// - parameter attempToFulfill: Closure in charge of generating the value to be emitted. - /// - attention: The closure is kept till a greater-than-zero demand is received (at which point, it is executed and then deleted). - @inlinable public init(_ attempToFulfill: @escaping Closure) { - self.closure = attempToFulfill + /// The promise returning the value (or failure) of the whole publisher. + public typealias Promise = (Result) -> Void + /// The closure type being store for delayed execution. + public typealias Closure = (_ promise: @escaping Promise) -> Void + + /// Deferred closure. + /// - attention: The closure is kept till a greater-than-zero demand is received (at which point, it is executed and then deleted). + public let closure: Closure + + /// Creates a publisher that send a value and completes successfully or just fails depending on the result of the given closure. + /// - parameter attempToFulfill: Closure in charge of generating the value to be emitted. + /// - attention: The closure is kept till a greater-than-zero demand is received (at which point, it is executed and then deleted). + @inlinable public init(_ attempToFulfill: @escaping Closure) { + self.closure = attempToFulfill + } + + public func receive(subscriber: S) where S: Subscriber, S.Input==Output, S.Failure==Failure { + let subscription = _Conduit(downstream: subscriber, closure: self.closure) + subscriber.receive(subscription: subscription) + } +} + +private extension DeferredFuture { + /// The shadow subscription chain's origin. + final class _Conduit: Subscription where Downstream:Subscriber, Downstream.Input==Output, Downstream.Failure==Failure { + /// Enum listing all possible conduit states. + @ConduitLock private var state: ConduitState + + init(downstream: Downstream, closure: @escaping Closure) { + self.state = .active(_Configuration(downstream: downstream, step: .awaitingExecution(closure))) } - - public func receive(subscriber: S) where S: Subscriber, S.Input==Output, S.Failure==Failure { - let subscription = Conduit(downstream: subscriber, closure: self.closure) - subscriber.receive(subscription: subscription) + + deinit { + self._state.invalidate() } -} -fileprivate extension DeferredFuture { - /// The shadow subscription chain's origin. - final class Conduit: Subscription where Downstream:Subscriber, Downstream.Input==Output, Downstream.Failure==Failure { - /// Enum listing all possible conduit states. - @ConduitLock private var state: ConduitState - - init(downstream: Downstream, closure: @escaping Closure) { - self.state = .active(_Configuration(downstream: downstream, step: .awaitingExecution(closure))) - } - - deinit { - self._state.invalidate() - } - - func request(_ demand: Subscribers.Demand) { - guard demand > 0 else { return } - - self._state.lock() - guard var config = self._state.value.activeConfiguration, - case .awaitingExecution(let closure) = config.step else { return self._state.unlock() } - config.step = .awaitingPromise - self._state.value = .active(config) - self._state.unlock() - - closure { [weak self] (result) in - guard let self = self else { return } - - guard case .active(let config) = self._state.terminate() else { return } - guard case .awaitingPromise = config.step else { fatalError() } - - switch result { - case .success(let value): - _ = config.downstream.receive(value) - config.downstream.receive(completion: .finished) - case .failure(let error): - config.downstream.receive(completion: .failure(error)) - } - } - } - - func cancel() { - self._state.terminate() + func request(_ demand: Subscribers.Demand) { + guard demand > 0 else { return } + + self._state.lock() + guard var config = self._state.value.activeConfiguration, + case .awaitingExecution(let closure) = config.step else { return self._state.unlock() } + config.step = .awaitingPromise + self._state.value = .active(config) + self._state.unlock() + + closure { [weak self] (result) in + guard let self = self else { return } + + guard case .active(let config) = self._state.terminate() else { return } + guard case .awaitingPromise = config.step else { fatalError() } + + switch result { + case .success(let value): + _ = config.downstream.receive(value) + config.downstream.receive(completion: .finished) + case .failure(let error): + config.downstream.receive(completion: .failure(error)) } + } } + + func cancel() { + self._state.terminate() + } + } } -private extension DeferredFuture.Conduit { - /// Values needed for the subscription's active state. - struct _Configuration { - /// The downstream subscriber awaiting any value and/or completion events. - let downstream: Downstream - /// The state on the promise execution - var step: Step - - enum Step { - /// The closure hasn't been executed. - case awaitingExecution(_ closure: DeferredFuture.Closure) - /// The closure has been executed, but no promise has been received yet. - case awaitingPromise - } +private extension DeferredFuture._Conduit { + /// Values needed for the subscription's active state. + struct _Configuration { + /// The downstream subscriber awaiting any value and/or completion events. + let downstream: Downstream + /// The state on the promise execution + var step: Step + + enum Step { + /// The closure hasn't been executed. + case awaitingExecution(_ closure: DeferredFuture.Closure) + /// The closure has been executed, but no promise has been received yet. + case awaitingPromise } + } } diff --git a/sources/runtime/publishers/DeferredPassthrough.swift b/sources/runtime/publishers/DeferredPassthrough.swift index c76e0f5..a955cc2 100644 --- a/sources/runtime/publishers/DeferredPassthrough.swift +++ b/sources/runtime/publishers/DeferredPassthrough.swift @@ -9,98 +9,98 @@ import Combine /// - The closure will get *cleaned up* as soon as it returns. /// - remark: Please notice, the pipeline won't complete if the subject within the closure doesn't forwards `.send(completion:)`. public struct DeferredPassthrough: Publisher { - /// The closure type being store for delayed execution. - public typealias Closure = (PassthroughSubject) -> Void - - /// Publisher's closure storage. - /// - note: The closure is kept in the publisher, thus if you keep the publisher around any reference in the closure will be kept too. - public let closure: Closure - /// Creates a publisher that sends - /// - parameter setup: The closure for delayed execution. - /// - remark: Please notice, the pipeline won't complete if the subject within the closure doesn't send `.send(completion:)`. - @inlinable public init(_ setup: @escaping Closure) { - self.closure = setup + /// The closure type being store for delayed execution. + public typealias Closure = (PassthroughSubject) -> Void + + /// Publisher's closure storage. + /// - note: The closure is kept in the publisher, thus if you keep the publisher around any reference in the closure will be kept too. + public let closure: Closure + /// Creates a publisher that sends + /// - parameter setup: The closure for delayed execution. + /// - remark: Please notice, the pipeline won't complete if the subject within the closure doesn't send `.send(completion:)`. + @inlinable public init(_ setup: @escaping Closure) { + self.closure = setup + } + + public func receive(subscriber: S) where S:Subscriber, S.Input==Output, S.Failure==Failure { + let upstream = PassthroughSubject() + let conduit = _Conduit(upstream: upstream, downstream: subscriber, closure: self.closure) + upstream.subscribe(conduit) + } +} + +private extension DeferredPassthrough { + /// Internal Shadow subscription catching all messages from downstream and forwarding them upstream. + final class _Conduit: Subscription, Subscriber where Downstream:Subscriber, Downstream.Input==Output, Downstream.Failure==Failure { + /// Enum listing all possible conduit states. + @ConduitLock private var state: ConduitState<_WaitConfiguration,_ActiveConfiguration> + + /// Designated initializer passing all the needed info (except the upstream subscription). + init(upstream: PassthroughSubject, downstream: Downstream, closure: @escaping Closure) { + self.state = .awaitingSubscription(_WaitConfiguration(upstream: upstream, downstream: downstream, closure: closure)) } - public func receive(subscriber: S) where S:Subscriber, S.Input==Output, S.Failure==Failure { - let upstream = PassthroughSubject() - let conduit = Conduit(upstream: upstream, downstream: subscriber, closure: self.closure) - upstream.subscribe(conduit) + deinit { + self.cancel() + self._state.invalidate() } -} -fileprivate extension DeferredPassthrough { - /// Internal Shadow subscription catching all messages from downstream and forwarding them upstream. - final class Conduit: Subscription, Subscriber where Downstream:Subscriber, Downstream.Input==Output, Downstream.Failure==Failure { - /// Enum listing all possible conduit states. - @ConduitLock private var state: ConduitState<_WaitConfiguration,_ActiveConfiguration> - - /// Designated initializer passing all the needed info (except the upstream subscription). - init(upstream: PassthroughSubject, downstream: Downstream, closure: @escaping Closure) { - self.state = .awaitingSubscription(_WaitConfiguration(upstream: upstream, downstream: downstream, closure: closure)) - } - - deinit { - self.cancel() - self._state.invalidate() - } - - func receive(subscription: Subscription) { - guard let config = self._state.activate(atomic: { _ActiveConfiguration(upstream: subscription, downstream: $0.downstream, setup: ($0.upstream, $0.closure)) }) else { - return subscription.cancel() - } - config.downstream.receive(subscription: self) - } - - func request(_ demand: Subscribers.Demand) { - guard demand > 0 else { return } - - self._state.lock() - guard let config = self._state.value.activeConfiguration else { return self._state.unlock() } - self._state.value = .active(.init(upstream: config.upstream, downstream: config.downstream, setup: nil)) - self._state.unlock() - - config.upstream.request(demand) - guard let (subject, closure) = config.setup else { return } - closure(subject) - } - - func receive(_ input: Output) -> Subscribers.Demand { - self._state.lock() - guard let config = self._state.value.activeConfiguration else { - self._state.unlock() - return .none - } - self._state.unlock() - return config.downstream.receive(input) - } - - func receive(completion: Subscribers.Completion) { - guard case .active(let config) = self._state.terminate() else { return } - config.downstream.receive(completion: completion) - } - - func cancel() { - guard case .active(let config) = self._state.terminate() else { return } - config.upstream.cancel() - } + func receive(subscription: Subscription) { + guard let config = self._state.activate(atomic: { _ActiveConfiguration(upstream: subscription, downstream: $0.downstream, setup: ($0.upstream, $0.closure)) }) else { + return subscription.cancel() + } + config.downstream.receive(subscription: self) } -} -private extension DeferredPassthrough.Conduit { - /// Values needed for the subscription's awaiting state. - struct _WaitConfiguration { - let upstream: PassthroughSubject - let downstream: Downstream - let closure: DeferredPassthrough.Closure + func request(_ demand: Subscribers.Demand) { + guard demand > 0 else { return } + + self._state.lock() + guard let config = self._state.value.activeConfiguration else { return self._state.unlock() } + self._state.value = .active(.init(upstream: config.upstream, downstream: config.downstream, setup: nil)) + self._state.unlock() + + config.upstream.request(demand) + guard let (subject, closure) = config.setup else { return } + closure(subject) } - - /// Values needed for the subscription's active state. - struct _ActiveConfiguration { - typealias Setup = (subject: PassthroughSubject, closure: DeferredPassthrough.Closure) - - let upstream: Subscription - let downstream: Downstream - var setup: Setup? + + func receive(_ input: Output) -> Subscribers.Demand { + self._state.lock() + guard let config = self._state.value.activeConfiguration else { + self._state.unlock() + return .none + } + self._state.unlock() + return config.downstream.receive(input) + } + + func receive(completion: Subscribers.Completion) { + guard case .active(let config) = self._state.terminate() else { return } + config.downstream.receive(completion: completion) + } + + func cancel() { + guard case .active(let config) = self._state.terminate() else { return } + config.upstream.cancel() } + } +} + +private extension DeferredPassthrough._Conduit { + /// Values needed for the subscription's awaiting state. + struct _WaitConfiguration { + let upstream: PassthroughSubject + let downstream: Downstream + let closure: DeferredPassthrough.Closure + } + + /// Values needed for the subscription's active state. + struct _ActiveConfiguration { + typealias Setup = (subject: PassthroughSubject, closure: DeferredPassthrough.Closure) + + let upstream: Subscription + let downstream: Downstream + var setup: Setup? + } } diff --git a/sources/runtime/publishers/DeferredResult.swift b/sources/runtime/publishers/DeferredResult.swift index d18f942..a2bd958 100644 --- a/sources/runtime/publishers/DeferredResult.swift +++ b/sources/runtime/publishers/DeferredResult.swift @@ -4,64 +4,64 @@ import Combine /// /// This publisher is used at the origin of a publisher chain and it only provides the value when it receives a request with a demand greater than zero. public struct DeferredResult: Publisher { - /// The closure type being store for delayed execution. - public typealias Closure = () -> Result - /// Deferred closure. - /// - attention: The closure is kept till a greater-than-zero demand is received (at which point, it is executed and then deleted). - public let closure: Closure - - /// Creates a publisher that send a value and completes successfully or just fails depending on the result of the given closure. - /// - parameter closure: Closure in charge of generating the value to be emitted. - /// - attention: The closure is kept till a greater-than-zero demand is received (at which point, it is executed and then deleted). - @inlinable public init(closure: @escaping Closure) { - self.closure = closure + /// The closure type being store for delayed execution. + public typealias Closure = () -> Result + /// Deferred closure. + /// - attention: The closure is kept till a greater-than-zero demand is received (at which point, it is executed and then deleted). + public let closure: Closure + + /// Creates a publisher that send a value and completes successfully or just fails depending on the result of the given closure. + /// - parameter closure: Closure in charge of generating the value to be emitted. + /// - attention: The closure is kept till a greater-than-zero demand is received (at which point, it is executed and then deleted). + @inlinable public init(closure: @escaping Closure) { + self.closure = closure + } + + public func receive(subscriber: S) where S: Subscriber, S.Input==Output, S.Failure==Failure { + let subscription = _Conduit(downstream: subscriber, closure: self.closure) + subscriber.receive(subscription: subscription) + } +} + +private extension DeferredResult { + /// The shadow subscription chain's origin. + final class _Conduit: Subscription where Downstream:Subscriber, Downstream.Input==Output, Downstream.Failure==Failure { + /// Enum listing all possible conduit states. + @ConduitLock private var state: ConduitState + + /// Designated initializer passing the state configuration values. + init(downstream: Downstream, closure: @escaping Closure) { + self.state = .active(_Configuration(downstream: downstream, closure: closure)) } - - public func receive(subscriber: S) where S: Subscriber, S.Input==Output, S.Failure==Failure { - let subscription = Conduit(downstream: subscriber, closure: self.closure) - subscriber.receive(subscription: subscription) + + deinit { + self._state.invalidate() } -} -fileprivate extension DeferredResult { - /// The shadow subscription chain's origin. - final class Conduit: Subscription where Downstream:Subscriber, Downstream.Input==Output, Downstream.Failure==Failure { - /// Enum listing all possible conduit states. - @ConduitLock private var state: ConduitState - - /// Designated initializer passing the state configuration values. - init(downstream: Downstream, closure: @escaping Closure) { - self.state = .active(_Configuration(downstream: downstream, closure: closure)) - } - - deinit { - self._state.invalidate() - } - - func request(_ demand: Subscribers.Demand) { - guard demand > 0, case .active(let config) = self._state.terminate() else { return } - - switch config.closure() { - case .success(let value): - _ = config.downstream.receive(value) - config.downstream.receive(completion: .finished) - case .failure(let error): - config.downstream.receive(completion: .failure(error)) - } - } - - func cancel() { - self._state.terminate() - } + func request(_ demand: Subscribers.Demand) { + guard demand > 0, case .active(let config) = self._state.terminate() else { return } + + switch config.closure() { + case .success(let value): + _ = config.downstream.receive(value) + config.downstream.receive(completion: .finished) + case .failure(let error): + config.downstream.receive(completion: .failure(error)) + } } -} -private extension DeferredResult.Conduit { - /// Values needed for the subscription's active state. - struct _Configuration { - /// The downstream subscriber awaiting any value and/or completion events. - let downstream: Downstream - /// The closure generating the successful/failure completion. - let closure: DeferredResult.Closure + func cancel() { + self._state.terminate() } + } +} + +private extension DeferredResult._Conduit { + /// Values needed for the subscription's active state. + struct _Configuration { + /// The downstream subscriber awaiting any value and/or completion events. + let downstream: Downstream + /// The closure generating the successful/failure completion. + let closure: DeferredResult.Closure + } } diff --git a/sources/runtime/publishers/DeferredTryComplete.swift b/sources/runtime/publishers/DeferredTryComplete.swift index 26ab283..78ff884 100644 --- a/sources/runtime/publishers/DeferredTryComplete.swift +++ b/sources/runtime/publishers/DeferredTryComplete.swift @@ -4,70 +4,70 @@ import Combine /// /// This publisher is used at the origin of a publisher chain and it only provides the value when it receives a request with a demand greater than zero. public struct DeferredTryComplete: Publisher { - public typealias Failure = Swift.Error - /// The closure type being store for delayed execution. - public typealias Closure = () throws -> Void - - /// Deferred closure. - /// - attention: The closure is kept till a greater-than-zero demand is received (at which point, it is executed and then deleted). - public let closure: Closure - - /// Creates a publisher that send a successful completion once it receives a positive request (i.e. a request greater than zero) - public init() { - self.closure = { return } + public typealias Failure = Swift.Error + /// The closure type being store for delayed execution. + public typealias Closure = () throws -> Void + + /// Deferred closure. + /// - attention: The closure is kept till a greater-than-zero demand is received (at which point, it is executed and then deleted). + public let closure: Closure + + /// Creates a publisher that send a successful completion once it receives a positive request (i.e. a request greater than zero) + public init() { + self.closure = { return } + } + + /// Creates a publisher that send a value and completes successfully or just fails depending on the result of the given closure. + /// - parameter closure: The closure which produces an empty successful completion or a failure (if it throws). + /// - attention: The closure is kept till a greater-than-zero demand is received (at which point, it is executed and then deleted). + @inlinable public init(closure: @escaping Closure) { + self.closure = closure + } + + public func receive(subscriber: S) where S:Subscriber, S.Input==Output, S.Failure==Failure { + let subscription = _Conduit(downstream: subscriber, closure: self.closure) + subscriber.receive(subscription: subscription) + } +} + +private extension DeferredTryComplete { + /// The shadow subscription chain's origin. + final class _Conduit: Subscription where Downstream:Subscriber, Downstream.Failure==Failure { + /// Enum listing all possible conduit states. + @ConduitLock private var state: ConduitState + + init(downstream: Downstream, closure: @escaping Closure) { + self.state = .active(_Configuration(downstream: downstream, closure: closure)) } - - /// Creates a publisher that send a value and completes successfully or just fails depending on the result of the given closure. - /// - parameter closure: The closure which produces an empty successful completion or a failure (if it throws). - /// - attention: The closure is kept till a greater-than-zero demand is received (at which point, it is executed and then deleted). - @inlinable public init(closure: @escaping Closure) { - self.closure = closure + + deinit { + self._state.invalidate() } - - public func receive(subscriber: S) where S:Subscriber, S.Input==Output, S.Failure==Failure { - let subscription = Conduit(downstream: subscriber, closure: self.closure) - subscriber.receive(subscription: subscription) + + func request(_ demand: Subscribers.Demand) { + guard demand > 0, case .active(let config) = self._state.terminate() else { return } + + do { + try config.closure() + } catch let error { + return config.downstream.receive(completion: .failure(error)) + } + + config.downstream.receive(completion: .finished) } -} -fileprivate extension DeferredTryComplete { - /// The shadow subscription chain's origin. - final class Conduit: Subscription where Downstream:Subscriber, Downstream.Failure==Failure { - /// Enum listing all possible conduit states. - @ConduitLock private var state: ConduitState - - init(downstream: Downstream, closure: @escaping Closure) { - self.state = .active(_Configuration(downstream: downstream, closure: closure)) - } - - deinit { - self._state.invalidate() - } - - func request(_ demand: Subscribers.Demand) { - guard demand > 0, case .active(let config) = self._state.terminate() else { return } - - do { - try config.closure() - } catch let error { - return config.downstream.receive(completion: .failure(error)) - } - - config.downstream.receive(completion: .finished) - } - - func cancel() { - self._state.terminate() - } + func cancel() { + self._state.terminate() } + } } -private extension DeferredTryComplete.Conduit { - /// Values needed for the subscription's active state. - struct _Configuration { - /// The downstream subscriber awaiting any value and/or completion events. - let downstream: Downstream - /// The closure generating the successful/failure completion. - let closure: DeferredTryComplete.Closure - } +private extension DeferredTryComplete._Conduit { + /// Values needed for the subscription's active state. + struct _Configuration { + /// The downstream subscriber awaiting any value and/or completion events. + let downstream: Downstream + /// The closure generating the successful/failure completion. + let closure: DeferredTryComplete.Closure + } } diff --git a/sources/runtime/publishers/DeferredTryValue.swift b/sources/runtime/publishers/DeferredTryValue.swift index d95a8d8..0a94693 100644 --- a/sources/runtime/publishers/DeferredTryValue.swift +++ b/sources/runtime/publishers/DeferredTryValue.swift @@ -4,69 +4,69 @@ import Combine /// /// This publisher is used at the origin of a publisher chain and it only executes the passed closure when it receives a request with a demand greater than zero. public struct DeferredTryValue: Publisher { - public typealias Failure = Swift.Error - /// The closure type being store for delayed execution. - public typealias Closure = () throws -> Output - /// Deferred closure. - /// - attention: The closure is kept till a greater-than-zero demand is received (at which point, it is executed and then deleted). - public let closure: Closure - - /// Creates a publisher which will a value and completes successfully, or just fail depending on the result of the given closure. - /// - parameter closure: Closure in charge of generating the value to be emitted. - /// - attention: The closure is kept till a greater-than-zero demand is received (at which point, it is executed and then deleted). - @inlinable public init(closure: @escaping Closure) { - self.closure = closure + public typealias Failure = Swift.Error + /// The closure type being store for delayed execution. + public typealias Closure = () throws -> Output + /// Deferred closure. + /// - attention: The closure is kept till a greater-than-zero demand is received (at which point, it is executed and then deleted). + public let closure: Closure + + /// Creates a publisher which will a value and completes successfully, or just fail depending on the result of the given closure. + /// - parameter closure: Closure in charge of generating the value to be emitted. + /// - attention: The closure is kept till a greater-than-zero demand is received (at which point, it is executed and then deleted). + @inlinable public init(closure: @escaping Closure) { + self.closure = closure + } + + public func receive(subscriber: S) where S:Subscriber, S.Input==Output, S.Failure==Failure { + let subscription = _Conduit(downstream: subscriber, closure: self.closure) + subscriber.receive(subscription: subscription) + } +} + +private extension DeferredTryValue { + /// The shadow subscription chain's origin. + final class _Conduit: Subscription where Downstream:Subscriber, Downstream.Input==Output, Downstream.Failure==Failure { + /// Enum listing all possible conduit states. + @ConduitLock private var state: ConduitState + + /// Sets up the guarded state. + /// - parameter downstream: Downstream subscriber receiving the data from this instance. + /// - parameter closure: Closure in charge of generating the emitted value. + init(downstream: Downstream, closure: @escaping Closure) { + self.state = .active(_Configuration(downstream: downstream, closure: closure)) + } + + deinit { + self._state.invalidate() } - - public func receive(subscriber: S) where S:Subscriber, S.Input==Output, S.Failure==Failure { - let subscription = Conduit(downstream: subscriber, closure: self.closure) - subscriber.receive(subscription: subscription) + + func request(_ demand: Subscribers.Demand) { + guard demand > 0, case .active(let config) = self._state.terminate() else { return } + + let input: Output + do { + input = try config.closure() + } catch let error { + return config.downstream.receive(completion: .failure(error)) + } + + _ = config.downstream.receive(input) + config.downstream.receive(completion: .finished) } -} -fileprivate extension DeferredTryValue { - /// The shadow subscription chain's origin. - final class Conduit: Subscription where Downstream:Subscriber, Downstream.Input==Output, Downstream.Failure==Failure { - /// Enum listing all possible conduit states. - @ConduitLock private var state: ConduitState - - /// Sets up the guarded state. - /// - parameter downstream: Downstream subscriber receiving the data from this instance. - /// - parameter closure: Closure in charge of generating the emitted value. - init(downstream: Downstream, closure: @escaping Closure) { - self.state = .active(_Configuration(downstream: downstream, closure: closure)) - } - - deinit { - self._state.invalidate() - } - - func request(_ demand: Subscribers.Demand) { - guard demand > 0, case .active(let config) = self._state.terminate() else { return } - - let input: Output - do { - input = try config.closure() - } catch let error { - return config.downstream.receive(completion: .failure(error)) - } - - _ = config.downstream.receive(input) - config.downstream.receive(completion: .finished) - } - - func cancel() { - self._state.terminate() - } + func cancel() { + self._state.terminate() } + } } -private extension DeferredTryValue.Conduit { - /// Values needed for the subscription's active state. - struct _Configuration { - /// The downstream subscriber awaiting any value and/or completion events. - let downstream: Downstream - /// The closure generating the optional value and successful/failure completion. - let closure: DeferredTryValue.Closure - } +private extension DeferredTryValue._Conduit { + /// Values needed for the subscription's active state. + struct _Configuration { + /// The downstream subscriber awaiting any value and/or completion events. + let downstream: Downstream + /// The closure generating the optional value and successful/failure completion. + let closure: DeferredTryValue.Closure + } } diff --git a/sources/runtime/publishers/DeferredValue.swift b/sources/runtime/publishers/DeferredValue.swift index a0ee0a4..a96b1a8 100644 --- a/sources/runtime/publishers/DeferredValue.swift +++ b/sources/runtime/publishers/DeferredValue.swift @@ -4,62 +4,62 @@ import Combine /// /// This publisher is used at the origin of a publisher chain and it ony executes the passed closure when it receives a request with a demand greater than zero. public struct DeferredValue: Publisher where Failure:Swift.Error { - /// The closure type being store for delayed execution. - public typealias Closure = () -> Output - /// Deferred closure. - /// - attention: The closure is kept till a greater-than-zero demand is received (at which point, it is executed and then deleted). - public let closure: Closure - - /// Creates a publisher which will a value and completes successfully, or just fail depending on the result of the given closure. - /// - parameter closure: Closure in charge of generating the value to be emitted. - /// - attention: The closure is kept till a greater-than-zero demand is received (at which point, it is executed and then deleted). - @inlinable public init(failure: Failure.Type = Failure.self, closure: @escaping Closure) { - self.closure = closure + /// The closure type being store for delayed execution. + public typealias Closure = () -> Output + /// Deferred closure. + /// - attention: The closure is kept till a greater-than-zero demand is received (at which point, it is executed and then deleted). + public let closure: Closure + + /// Creates a publisher which will a value and completes successfully, or just fail depending on the result of the given closure. + /// - parameter closure: Closure in charge of generating the value to be emitted. + /// - attention: The closure is kept till a greater-than-zero demand is received (at which point, it is executed and then deleted). + @inlinable public init(failure: Failure.Type = Failure.self, closure: @escaping Closure) { + self.closure = closure + } + + public func receive(subscriber: S) where S:Subscriber, S.Input==Output, S.Failure==Failure { + let subscription = _Conduit(downstream: subscriber, closure: self.closure) + subscriber.receive(subscription: subscription) + } +} + +private extension DeferredValue { + /// The shadow subscription chain's origin. + final class _Conduit: Subscription where Downstream:Subscriber, Downstream.Input==Output, Downstream.Failure==Failure { + /// Enum listing all possible conduit states. + @ConduitLock private var state: ConduitState<(),_Configuration> + + /// Sets up the guarded state. + /// - parameter downstream: Downstream subscriber receiving the data from this instance. + /// - parameter closure: Closure in charge of generating the emitted value. + init(downstream: Downstream, closure: @escaping Closure) { + self.state = .active(_Configuration(downstream: downstream, closure: closure)) } - - public func receive(subscriber: S) where S:Subscriber, S.Input==Output, S.Failure==Failure { - let subscription = Conduit(downstream: subscriber, closure: self.closure) - subscriber.receive(subscription: subscription) + + deinit { + self._state.invalidate() } -} -fileprivate extension DeferredValue { - /// The shadow subscription chain's origin. - final class Conduit: Subscription where Downstream:Subscriber, Downstream.Input==Output, Downstream.Failure==Failure { - /// Enum listing all possible conduit states. - @ConduitLock private var state: ConduitState<(),_Configuration> - - /// Sets up the guarded state. - /// - parameter downstream: Downstream subscriber receiving the data from this instance. - /// - parameter closure: Closure in charge of generating the emitted value. - init(downstream: Downstream, closure: @escaping Closure) { - self.state = .active(_Configuration(downstream: downstream, closure: closure)) - } - - deinit { - self._state.invalidate() - } - - func request(_ demand: Subscribers.Demand) { - guard demand > 0, - case .active(let config) = self._state.terminate() else { return } - - _ = config.downstream.receive(config.closure()) - config.downstream.receive(completion: .finished) - } - - func cancel() { - self._state.terminate() - } + func request(_ demand: Subscribers.Demand) { + guard demand > 0, + case .active(let config) = self._state.terminate() else { return } + + _ = config.downstream.receive(config.closure()) + config.downstream.receive(completion: .finished) } -} -private extension DeferredValue.Conduit { - /// Values needed for the subscription's active state. - struct _Configuration { - /// The downstream subscriber awaiting any value and/or completion events. - let downstream: Downstream - /// The closure generating the optional value and/or the successful/failure completion. - let closure: DeferredValue.Closure + func cancel() { + self._state.terminate() } + } +} + +private extension DeferredValue._Conduit { + /// Values needed for the subscription's active state. + struct _Configuration { + /// The downstream subscriber awaiting any value and/or completion events. + let downstream: Downstream + /// The closure generating the optional value and/or the successful/failure completion. + let closure: DeferredValue.Closure + } } diff --git a/sources/runtime/publishers/HandleEnd.swift b/sources/runtime/publishers/HandleEnd.swift index facd5de..799dfda 100644 --- a/sources/runtime/publishers/HandleEnd.swift +++ b/sources/runtime/publishers/HandleEnd.swift @@ -1,112 +1,112 @@ import Combine extension Publishers { - /// A publisher that performs the specified closure when the publisher completes or get cancelled. - public struct HandleEnd: Publisher where Upstream:Publisher { - public typealias Output = Upstream.Output - public typealias Failure = Upstream.Failure - /// Closure getting executed once the publisher receives a completion event or when the publisher gets cancelled. - /// - parameter completion: A completion event if the publisher completes (whether successfully or not), or `nil` in case the publisher is cancelled. - public typealias Closure = (_ completion: Subscribers.Completion?) -> Void - - /// Publisher emitting the events being received here. - public let upstream: Upstream - /// Closure executing the *ending* event. - public let closure: Closure - - /// Designated initializer providing the upstream publisher and the closure receiving the *ending* event. - /// - parameter upstream: Upstream publisher chain. - /// - parameter handle: A closure that executes when the publisher receives a completion event or when the publisher gets cancelled. - @inlinable public init(upstream: Upstream, handle: @escaping Closure) { - self.upstream = upstream - self.closure = handle - } - - public func receive(subscriber: S) where S:Subscriber, S.Input==Output, S.Failure==Failure { - let conduit = Conduit(downstream: subscriber, closure: self.closure) - self.upstream.subscribe(conduit) - } + /// A publisher that performs the specified closure when the publisher completes or get cancelled. + public struct HandleEnd: Publisher where Upstream:Publisher { + public typealias Output = Upstream.Output + public typealias Failure = Upstream.Failure + /// Closure getting executed once the publisher receives a completion event or when the publisher gets cancelled. + /// - parameter completion: A completion event if the publisher completes (whether successfully or not), or `nil` in case the publisher is cancelled. + public typealias Closure = (_ completion: Subscribers.Completion?) -> Void + + /// Publisher emitting the events being received here. + public let upstream: Upstream + /// Closure executing the *ending* event. + public let closure: Closure + + /// Designated initializer providing the upstream publisher and the closure receiving the *ending* event. + /// - parameter upstream: Upstream publisher chain. + /// - parameter handle: A closure that executes when the publisher receives a completion event or when the publisher gets cancelled. + @inlinable public init(upstream: Upstream, handle: @escaping Closure) { + self.upstream = upstream + self.closure = handle } -} -fileprivate extension Publishers.HandleEnd { - /// Represents an active `HandleEnd` publisher taking both the role of `Subscriber` (for upstream publishers) and `Subscription` (for downstream subscribers). - final class Conduit: Subscription, Subscriber where Downstream:Subscriber, Downstream.Input==Output, Downstream.Failure==Failure { - typealias Input = Upstream.Output - typealias Failure = Upstream.Failure - /// Enum listing all possible states. - @ConduitLock private var state: ConduitState<_WaitConfiguration,_ActiveConfiguration> - - init(downstream: Downstream, closure: @escaping Closure) { - self.state = .awaitingSubscription(.init(closure: closure, downstream: downstream)) - } - - deinit { - self.cancel() - self._state.invalidate() - } - - func receive(subscription: Subscription) { - guard let config = self._state.activate(atomic: { .init(upstream: subscription, closure: $0.closure, downstream: $0.downstream) }) else { - return subscription.cancel() - } - config.downstream.receive(subscription: self) - } - - func request(_ demand: Subscribers.Demand) { - self._state.lock() - guard let config = self._state.value.activeConfiguration else { return self._state.unlock() } - self._state.unlock() - config.upstream.request(demand) - } - - func receive(_ input: Upstream.Output) -> Subscribers.Demand { - self._state.lock() - guard let config = self._state.value.activeConfiguration else { self._state.unlock(); return .unlimited } - self._state.unlock() - - return config.downstream.receive(input) - } - - func receive(completion: Subscribers.Completion) { - switch self._state.terminate() { - case .active(let config): - config.closure(completion) - config.downstream.receive(completion: completion) - case .terminated: return - case .awaitingSubscription: fatalError() - } - } - - func cancel() { - switch self._state.terminate() { - case .awaitingSubscription(let config): - config.closure(nil) - case .active(let config): - config.closure(nil) - config.upstream.cancel() - case .terminated: return - } - } + public func receive(subscriber: S) where S:Subscriber, S.Input==Output, S.Failure==Failure { + let conduit = _Conduit(downstream: subscriber, closure: self.closure) + self.upstream.subscribe(conduit) } + } } -private extension Publishers.HandleEnd.Conduit { - /// The necessary variables during the *awaiting* stage. - struct _WaitConfiguration { - /// The closure being executed only once when the publisher completes or get cancelled. - let closure: Publishers.HandleEnd.Closure - /// The subscriber further down the chain. - let downstream: Downstream +private extension Publishers.HandleEnd { + /// Represents an active `HandleEnd` publisher taking both the role of `Subscriber` (for upstream publishers) and `Subscription` (for downstream subscribers). + final class _Conduit: Subscription, Subscriber where Downstream:Subscriber, Downstream.Input==Output, Downstream.Failure==Failure { + typealias Input = Upstream.Output + typealias Failure = Upstream.Failure + /// Enum listing all possible states. + @ConduitLock private var state: ConduitState<_WaitConfiguration,_ActiveConfiguration> + + init(downstream: Downstream, closure: @escaping Closure) { + self.state = .awaitingSubscription(.init(closure: closure, downstream: downstream)) + } + + deinit { + self.cancel() + self._state.invalidate() } - - /// The necessary variables during the *active* stage. - struct _ActiveConfiguration { - /// The upstream subscription. - let upstream: Subscription - /// The closure being executed only once when the publisher completes or get cancelled. - let closure: Publishers.HandleEnd.Closure - /// The subscriber further down the chain. - let downstream: Downstream + + func receive(subscription: Subscription) { + guard let config = self._state.activate(atomic: { .init(upstream: subscription, closure: $0.closure, downstream: $0.downstream) }) else { + return subscription.cancel() + } + config.downstream.receive(subscription: self) } + + func request(_ demand: Subscribers.Demand) { + self._state.lock() + guard let config = self._state.value.activeConfiguration else { return self._state.unlock() } + self._state.unlock() + config.upstream.request(demand) + } + + func receive(_ input: Upstream.Output) -> Subscribers.Demand { + self._state.lock() + guard let config = self._state.value.activeConfiguration else { self._state.unlock(); return .unlimited } + self._state.unlock() + + return config.downstream.receive(input) + } + + func receive(completion: Subscribers.Completion) { + switch self._state.terminate() { + case .active(let config): + config.closure(completion) + config.downstream.receive(completion: completion) + case .terminated: return + case .awaitingSubscription: fatalError() + } + } + + func cancel() { + switch self._state.terminate() { + case .awaitingSubscription(let config): + config.closure(nil) + case .active(let config): + config.closure(nil) + config.upstream.cancel() + case .terminated: return + } + } + } +} + +private extension Publishers.HandleEnd._Conduit { + /// The necessary variables during the *awaiting* stage. + struct _WaitConfiguration { + /// The closure being executed only once when the publisher completes or get cancelled. + let closure: Publishers.HandleEnd.Closure + /// The subscriber further down the chain. + let downstream: Downstream + } + + /// The necessary variables during the *active* stage. + struct _ActiveConfiguration { + /// The upstream subscription. + let upstream: Subscription + /// The closure being executed only once when the publisher completes or get cancelled. + let closure: Publishers.HandleEnd.Closure + /// The subscriber further down the chain. + let downstream: Downstream + } } diff --git a/sources/runtime/publishers/Retry.swift b/sources/runtime/publishers/Retry.swift index f449072..a8725dd 100644 --- a/sources/runtime/publishers/Retry.swift +++ b/sources/runtime/publishers/Retry.swift @@ -2,258 +2,258 @@ import Combine import Foundation extension Publishers { - /// A publisher that attempts to recreate its subscription to a failed upstream publisher waiting a given time interval between retries. - /// - /// Please notice that any value sent before a failure is still received downstream. - public struct DelayedRetry: Publisher where Upstream:Publisher, S:Scheduler { - public typealias Output = Upstream.Output - public typealias Failure = Upstream.Failure - - /// The upstream publisher. - public let upstream: Upstream - /// The scheduler used to wait for a specific time interval. - public let scheduler: S - /// The tolerance used when scheduling a new attempt after a failure. A default implies the minimum tolerance. - public let tolerance: S.SchedulerTimeType.Stride? - /// The options for the specified scheduler. - public let options: S.SchedulerOptions? - /// The amount of seconds being waited after a failure occurrence. Negative values are considered zero. - public let intervals: [TimeInterval] - /// Creates a publisher that transforms the incoming value into another value, but may respond at a time in the future. - /// - parameter upstream: The event emitter to the publisher being created. - /// - parameter scheduler: The scheduler used to wait for the specific intervals. - /// - parameter options: The options for the given scheduler. - /// - parameter intervals: The amount of seconds to wait after a failure occurrence. Negative values are considered zero. - @inlinable public init(upstream: Upstream, scheduler: S, tolerance: S.SchedulerTimeType.Stride? = nil, options: S.SchedulerOptions? = nil, intervals: [TimeInterval]) { - self.upstream = upstream - self.scheduler = scheduler - self.tolerance = tolerance - self.options = options - self.intervals = intervals - } - - public func receive(subscriber: S) where S:Subscriber, S.Input==Output, S.Failure==Failure { - let conduit = DelayedRetry.Conduit(upstream: self.upstream, downstream: subscriber, - scheduler: self.scheduler, tolerance: self.tolerance, options: self.options, - intervals: self.intervals) - self.upstream.subscribe(conduit) - } + /// A publisher that attempts to recreate its subscription to a failed upstream publisher waiting a given time interval between retries. + /// + /// Please notice that any value sent before a failure is still received downstream. + public struct DelayedRetry: Publisher where Upstream:Publisher, S:Scheduler { + public typealias Output = Upstream.Output + public typealias Failure = Upstream.Failure + + /// The upstream publisher. + public let upstream: Upstream + /// The scheduler used to wait for a specific time interval. + public let scheduler: S + /// The tolerance used when scheduling a new attempt after a failure. A default implies the minimum tolerance. + public let tolerance: S.SchedulerTimeType.Stride? + /// The options for the specified scheduler. + public let options: S.SchedulerOptions? + /// The amount of seconds being waited after a failure occurrence. Negative values are considered zero. + public let intervals: [TimeInterval] + /// Creates a publisher that transforms the incoming value into another value, but may respond at a time in the future. + /// - parameter upstream: The event emitter to the publisher being created. + /// - parameter scheduler: The scheduler used to wait for the specific intervals. + /// - parameter options: The options for the given scheduler. + /// - parameter intervals: The amount of seconds to wait after a failure occurrence. Negative values are considered zero. + @inlinable public init(upstream: Upstream, scheduler: S, tolerance: S.SchedulerTimeType.Stride? = nil, options: S.SchedulerOptions? = nil, intervals: [TimeInterval]) { + self.upstream = upstream + self.scheduler = scheduler + self.tolerance = tolerance + self.options = options + self.intervals = intervals + } + + public func receive(subscriber: S) where S:Subscriber, S.Input==Output, S.Failure==Failure { + let conduit = DelayedRetry._Conduit(upstream: self.upstream, downstream: subscriber, + scheduler: self.scheduler, tolerance: self.tolerance, options: self.options, + intervals: self.intervals) + self.upstream.subscribe(conduit) } + } } -fileprivate extension Publishers.DelayedRetry { - /// Represents an active `DelayedRetry` publisher taking both the role of `Subscriber` (for upstream publishers) and `Subscription` (for downstream subscribers). - final class Conduit: Subscription, Subscriber where Downstream:Subscriber, Downstream.Input==Output, Downstream.Failure==Failure { - typealias Input = Upstream.Output - typealias Failure = Upstream.Failure - /// Enum listing all possible conduit states. - @ConduitLock private var state: ConduitState<_WaitConfiguration,_ActiveConfiguration> - - init(upstream: Upstream, downstream: Downstream, scheduler: S, tolerance: S.SchedulerTimeType.Stride?, options: S.SchedulerOptions?, intervals: [TimeInterval]) { - self.state = .awaitingSubscription( - .init(publisher: upstream, downstream: downstream, - scheduler: scheduler, tolerance: tolerance, options: options, - intervals: intervals, next: 0, demand: .none) - ) - } - - deinit { - self.cancel() - self._state.invalidate() - } - - func receive(subscription: Subscription) { - guard let config = self._state.activate(atomic: { .init(upstream: subscription, config: $0) }) else { - return subscription.cancel() - } - - switch config.isPrime { - case true: config.downstream.receive(subscription: self) - case false: config.upstream.request(config.demand) - } - } - - func request(_ demand: Subscribers.Demand) { - guard demand > 0 else { return } - self._state.lock() - - switch self._state.value { - case .awaitingSubscription(let config): - guard !config.isPrime else { fatalError("A request cannot happen before a subscription has been performed") } - config.demand += demand - self._state.unlock() - case .active(let config): - config.demand += demand - self._state.unlock() - config.upstream.request(demand) - case .terminated: - self._state.unlock() - } - } - - func receive(_ input: Upstream.Output) -> Subscribers.Demand { - self._state.lock() - guard let config1 = self._state.value.activeConfiguration else { - self._state.unlock() - return .none - } - - config1.demand -= 1 - let downstream = config1.downstream - self._state.unlock() - - let demand = downstream.receive(input) - self._state.lock() - guard let config2 = self._state.value.activeConfiguration else { - self._state.unlock() - return .none - } - - config2.demand += demand - self._state.unlock() - return demand - } - - func receive(completion: Subscribers.Completion) { - self._state.lock() - guard let activeConfig = self._state.value.activeConfiguration else { - return self._state.unlock() - } - - guard case .failure = completion, let pause = activeConfig.nextPause else { - let downstream = activeConfig.downstream - self._state.value = .terminated - self._state.unlock() - return downstream.receive(completion: completion) - } - - self._state.value = .awaitingSubscription(.init(config: activeConfig)) - - guard pause > 0 else { - let publisher = activeConfig.publisher - self._state.unlock() - return publisher.subscribe(self) - } +private extension Publishers.DelayedRetry { + /// Represents an active `DelayedRetry` publisher taking both the role of `Subscriber` (for upstream publishers) and `Subscription` (for downstream subscribers). + final class _Conduit: Subscription, Subscriber where Downstream:Subscriber, Downstream.Input==Output, Downstream.Failure==Failure { + typealias Input = Upstream.Output + typealias Failure = Upstream.Failure + /// Enum listing all possible conduit states. + @ConduitLock private var state: ConduitState<_WaitConfiguration,_ActiveConfiguration> + + init(upstream: Upstream, downstream: Downstream, scheduler: S, tolerance: S.SchedulerTimeType.Stride?, options: S.SchedulerOptions?, intervals: [TimeInterval]) { + self.state = .awaitingSubscription( + .init(publisher: upstream, downstream: downstream, + scheduler: scheduler, tolerance: tolerance, options: options, + intervals: intervals, next: 0, demand: .none) + ) + } + + deinit { + self.cancel() + self._state.invalidate() + } + + func receive(subscription: Subscription) { + guard let config = self._state.activate(atomic: { .init(upstream: subscription, config: $0) }) else { + return subscription.cancel() + } + + switch config.isPrime { + case true: config.downstream.receive(subscription: self) + case false: config.upstream.request(config.demand) + } + } + + func request(_ demand: Subscribers.Demand) { + guard demand > 0 else { return } + self._state.lock() + + switch self._state.value { + case .awaitingSubscription(let config): + guard !config.isPrime else { fatalError("A request cannot happen before a subscription has been performed") } + config.demand += demand + self._state.unlock() + case .active(let config): + config.demand += demand + self._state.unlock() + config.upstream.request(demand) + case .terminated: + self._state.unlock() + } + } + + func receive(_ input: Upstream.Output) -> Subscribers.Demand { + self._state.lock() + guard let config1 = self._state.value.activeConfiguration else { + self._state.unlock() + return .none + } + + config1.demand -= 1 + let downstream = config1.downstream + self._state.unlock() + + let demand = downstream.receive(input) + self._state.lock() + guard let config2 = self._state.value.activeConfiguration else { + self._state.unlock() + return .none + } + + config2.demand += demand + self._state.unlock() + return demand + } + + func receive(completion: Subscribers.Completion) { + self._state.lock() + guard let activeConfig = self._state.value.activeConfiguration else { + return self._state.unlock() + } - let config = _WaitConfiguration(config: activeConfig) - let (scheduler, tolerance, options) = (config.scheduler, config.tolerance ?? config.scheduler.minimumTolerance, config.options) - self._state.unlock() - - let date = scheduler.now.advanced(by: .seconds(pause)) - scheduler.schedule(after: date, tolerance: tolerance, options: options) { [weak self] in - guard let self = self else { return } - self._state.lock() - guard let config = self._state.value.awaitingConfiguration else { return self._state.unlock() } - let publisher = config.publisher - self._state.unlock() - publisher.subscribe(self) - } - } - - func cancel() { - guard case .active(let config) = self._state.terminate() else { return } - config.upstream.cancel() - } + guard case .failure = completion, let pause = activeConfig.nextPause else { + let downstream = activeConfig.downstream + self._state.value = .terminated + self._state.unlock() + return downstream.receive(completion: completion) + } + + self._state.value = .awaitingSubscription(.init(config: activeConfig)) + + guard pause > 0 else { + let publisher = activeConfig.publisher + self._state.unlock() + return publisher.subscribe(self) + } + + let config = _WaitConfiguration(config: activeConfig) + let (scheduler, tolerance, options) = (config.scheduler, config.tolerance ?? config.scheduler.minimumTolerance, config.options) + self._state.unlock() + + let date = scheduler.now.advanced(by: .seconds(pause)) + scheduler.schedule(after: date, tolerance: tolerance, options: options) { [weak self] in + guard let self = self else { return } + self._state.lock() + guard let config = self._state.value.awaitingConfiguration else { return self._state.unlock() } + let publisher = config.publisher + self._state.unlock() + publisher.subscribe(self) + } + } + + func cancel() { + guard case .active(let config) = self._state.terminate() else { return } + config.upstream.cancel() } + } } -private extension Publishers.DelayedRetry.Conduit { - /// The necessary variables during the *awaiting* stage. - final class _WaitConfiguration { - /// The publisher to be initialized in case of problems. - let publisher: Upstream - /// The subscriber further down the chain. - let downstream: Downstream - /// The scheduler used to wait for the specific intervals. - let scheduler: S - /// The tolerance used when scheduling a new attempt after a failure. A default implies the minimum tolerance. - let tolerance: S.SchedulerTimeType.Stride? - /// The options for the given scheduler. - let options: S.SchedulerOptions? - /// The amount of seconds to wait after a failure occurrence. - let intervals: [TimeInterval] - /// The interval to use next if a failure is received. - var next: Int - /// The downstream requested demand. - var demand: Subscribers.Demand - - /// Boolean indicating whether the conduit has ever been subscribed to or not. - var isPrime: Bool { - return self.next == 0 - } - - /// Designated initializer. - init(publisher: Upstream, downstream: Downstream, scheduler: S, tolerance: S.SchedulerTimeType.Stride?, options: S.SchedulerOptions?, intervals: [TimeInterval], next: Int, demand: Subscribers.Demand) { - precondition(next >= 0) - self.publisher = publisher - self.downstream = downstream - self.scheduler = scheduler - self.tolerance = tolerance - self.options = options - self.intervals = intervals - self.next = next - self.demand = demand - } - - convenience init(config: _ActiveConfiguration) { - self.init(publisher: config.publisher, downstream: config.downstream, - scheduler: config.scheduler, tolerance: config.tolerance, options: config.options, - intervals: config.intervals, next: config.next, demand: config.demand) - } +private extension Publishers.DelayedRetry._Conduit { + /// The necessary variables during the *awaiting* stage. + final class _WaitConfiguration { + /// The publisher to be initialized in case of problems. + let publisher: Upstream + /// The subscriber further down the chain. + let downstream: Downstream + /// The scheduler used to wait for the specific intervals. + let scheduler: S + /// The tolerance used when scheduling a new attempt after a failure. A default implies the minimum tolerance. + let tolerance: S.SchedulerTimeType.Stride? + /// The options for the given scheduler. + let options: S.SchedulerOptions? + /// The amount of seconds to wait after a failure occurrence. + let intervals: [TimeInterval] + /// The interval to use next if a failure is received. + var next: Int + /// The downstream requested demand. + var demand: Subscribers.Demand + + /// Boolean indicating whether the conduit has ever been subscribed to or not. + var isPrime: Bool { + return self.next == 0 } - - /// The necessary variables during the *active* stage. - final class _ActiveConfiguration { - /// The publisher to be initialized in case of problems. - let publisher: Upstream - /// The upstream subscription. - let upstream: Subscription - /// The subscriber further down the chain. - let downstream: Downstream - /// The scheduler used to wait for the specific intervals. - let scheduler: S - /// The tolerance used when scheduling a new attempt after a failure. A default implies the minimum tolerance. - let tolerance: S.SchedulerTimeType.Stride? - /// The options for the given scheduler. - let options: S.SchedulerOptions? - /// The amount of seconds to wait after a failure occurrence. - let intervals: [TimeInterval] - /// The interval to use next if a failure is received. - var next: Int - /// The downstream requested demand. - var demand: Subscribers.Demand - - /// Boolean indicating whether it is the first ever attemp (primal attempt). - var isPrime: Bool { - return self.next == 0 - } - - /// Produces the next pause and increments the index integer. - var nextPause: TimeInterval? { - guard self.next < self.intervals.endIndex else { return nil } - let pause = self.intervals[self.next] - self.next += 1 - return pause - } - - /// Designated initializer. - init(publisher: Upstream, upstream: Subscription, downstream: Downstream, scheduler: S, tolerance: S.SchedulerTimeType.Stride?, options: S.SchedulerOptions?, intervals: [TimeInterval], next: Int, demand: Subscribers.Demand) { - precondition(next >= 0) - self.publisher = publisher - self.upstream = upstream - self.downstream = downstream - self.scheduler = scheduler - self.tolerance = tolerance - self.options = options - self.intervals = intervals - self.next = next - self.demand = demand - } - - convenience init(upstream: Subscription, config: _WaitConfiguration) { - self.init(publisher: config.publisher, upstream: upstream, downstream: config.downstream, - scheduler: config.scheduler, tolerance: config.tolerance, options: config.options, - intervals: config.intervals, next: config.next, demand: config.demand) - } + + /// Designated initializer. + init(publisher: Upstream, downstream: Downstream, scheduler: S, tolerance: S.SchedulerTimeType.Stride?, options: S.SchedulerOptions?, intervals: [TimeInterval], next: Int, demand: Subscribers.Demand) { + precondition(next >= 0) + self.publisher = publisher + self.downstream = downstream + self.scheduler = scheduler + self.tolerance = tolerance + self.options = options + self.intervals = intervals + self.next = next + self.demand = demand + } + + convenience init(config: _ActiveConfiguration) { + self.init(publisher: config.publisher, downstream: config.downstream, + scheduler: config.scheduler, tolerance: config.tolerance, options: config.options, + intervals: config.intervals, next: config.next, demand: config.demand) + } + } + + /// The necessary variables during the *active* stage. + final class _ActiveConfiguration { + /// The publisher to be initialized in case of problems. + let publisher: Upstream + /// The upstream subscription. + let upstream: Subscription + /// The subscriber further down the chain. + let downstream: Downstream + /// The scheduler used to wait for the specific intervals. + let scheduler: S + /// The tolerance used when scheduling a new attempt after a failure. A default implies the minimum tolerance. + let tolerance: S.SchedulerTimeType.Stride? + /// The options for the given scheduler. + let options: S.SchedulerOptions? + /// The amount of seconds to wait after a failure occurrence. + let intervals: [TimeInterval] + /// The interval to use next if a failure is received. + var next: Int + /// The downstream requested demand. + var demand: Subscribers.Demand + + /// Boolean indicating whether it is the first ever attemp (primal attempt). + var isPrime: Bool { + return self.next == 0 + } + + /// Produces the next pause and increments the index integer. + var nextPause: TimeInterval? { + guard self.next < self.intervals.endIndex else { return nil } + let pause = self.intervals[self.next] + self.next += 1 + return pause + } + + /// Designated initializer. + init(publisher: Upstream, upstream: Subscription, downstream: Downstream, scheduler: S, tolerance: S.SchedulerTimeType.Stride?, options: S.SchedulerOptions?, intervals: [TimeInterval], next: Int, demand: Subscribers.Demand) { + precondition(next >= 0) + self.publisher = publisher + self.upstream = upstream + self.downstream = downstream + self.scheduler = scheduler + self.tolerance = tolerance + self.options = options + self.intervals = intervals + self.next = next + self.demand = demand + } + + convenience init(upstream: Subscription, config: _WaitConfiguration) { + self.init(publisher: config.publisher, upstream: upstream, downstream: config.downstream, + scheduler: config.scheduler, tolerance: config.tolerance, options: config.options, + intervals: config.intervals, next: config.next, demand: config.demand) } + } } diff --git a/sources/runtime/publishers/Then.swift b/sources/runtime/publishers/Then.swift index 47b9a73..d033354 100644 --- a/sources/runtime/publishers/Then.swift +++ b/sources/runtime/publishers/Then.swift @@ -1,262 +1,262 @@ import Combine extension Publishers { - /// Transform the upstream successful completion event into a new or existing publisher. - public struct Then: Publisher where Upstream:Publisher, Child:Publisher, Upstream.Failure==Child.Failure { - public typealias Output = Child.Output - public typealias Failure = Child.Failure - /// Closure generating the publisher to be pipelined after upstream completes. - public typealias Closure = () -> Child - - /// Publisher emitting the events being received here. - public let upstream: Upstream - /// The maximum demand requested to the upstream at the same time. - public let maxDemand: Subscribers.Demand - /// Closure that will crete the publisher that will emit events downstream once a successful completion is received. - public let transform: Closure - - /// Designated initializer providing the upstream publisher and the closure in charge of arranging the transformation. - /// - /// The `maxDemand` must be greater than zero (`precondition`). - /// - parameter upstream: Upstream publisher chain which successful completion will trigger the `transform` closure. - /// - parameter maxDemand: The maximum demand requested to the upstream at the same time. - /// - parameter transfom: Closure providing the new (or existing) publisher. - @inlinable public init(upstream: Upstream, maxDemand: Subscribers.Demand = .unlimited, transform: @escaping ()->Child) { - precondition(maxDemand > .none) - self.upstream = upstream - self.maxDemand = maxDemand - self.transform = transform - } - - public func receive(subscriber: S) where S:Subscriber, S.Input==Output, S.Failure==Failure { - let conduitDownstream = DownstreamConduit(downstream: subscriber, transform: self.transform) - let conduitUpstream = UpstreamConduit(subscriber: conduitDownstream, maxDemand: self.maxDemand) - self.upstream.subscribe(conduitUpstream) - } + /// Transform the upstream successful completion event into a new or existing publisher. + public struct Then: Publisher where Upstream:Publisher, Child:Publisher, Upstream.Failure==Child.Failure { + public typealias Output = Child.Output + public typealias Failure = Child.Failure + /// Closure generating the publisher to be pipelined after upstream completes. + public typealias Closure = () -> Child + + /// Publisher emitting the events being received here. + public let upstream: Upstream + /// The maximum demand requested to the upstream at the same time. + public let maxDemand: Subscribers.Demand + /// Closure that will crete the publisher that will emit events downstream once a successful completion is received. + public let transform: Closure + + /// Designated initializer providing the upstream publisher and the closure in charge of arranging the transformation. + /// + /// The `maxDemand` must be greater than zero (`precondition`). + /// - parameter upstream: Upstream publisher chain which successful completion will trigger the `transform` closure. + /// - parameter maxDemand: The maximum demand requested to the upstream at the same time. + /// - parameter transfom: Closure providing the new (or existing) publisher. + @inlinable public init(upstream: Upstream, maxDemand: Subscribers.Demand = .unlimited, transform: @escaping ()->Child) { + precondition(maxDemand > .none) + self.upstream = upstream + self.maxDemand = maxDemand + self.transform = transform + } + + public func receive(subscriber: S) where S:Subscriber, S.Input==Output, S.Failure==Failure { + let conduitDownstream = _DownstreamConduit(downstream: subscriber, transform: self.transform) + let conduitUpstream = UpstreamConduit(subscriber: conduitDownstream, maxDemand: self.maxDemand) + self.upstream.subscribe(conduitUpstream) } + } } // MARK: - fileprivate extension Publishers.Then { - /// Helper that acts as a `Subscriber` for the upstream, but just forward events to the given `Conduit` instance. - final class UpstreamConduit: Subscription, Subscriber where Downstream:Subscriber, Downstream.Input==Child.Output, Downstream.Failure==Child.Failure { - typealias Input = Upstream.Output - typealias Failure = Upstream.Failure - - /// Enum listing all possible conduit states. - @ConduitLock private var state: ConduitState<_WaitConfiguration,_ActiveConfiguration> - /// The combine identifier shared with the `DownstreamConduit`. - let combineIdentifier: CombineIdentifier - /// The maximum demand requested to the upstream at the same time. - private let _maxDemand: Subscribers.Demand - - /// Designated initializer for this helper establishing the strong bond between the `Conduit` and the created helper. - init(subscriber: DownstreamConduit, maxDemand: Subscribers.Demand) { - precondition(maxDemand > .none) - self.state = .awaitingSubscription(_WaitConfiguration(downstream: subscriber)) - self.combineIdentifier = subscriber.combineIdentifier - self._maxDemand = maxDemand - } - - deinit { - self.cancel() - self._state.invalidate() - } - - func receive(subscription: Subscription) { - guard let config = self._state.activate(atomic: { _ActiveConfiguration(upstream: subscription, downstream: $0.downstream, didDownstreamRequestValues: false) }) else { - return subscription.cancel() - } - config.downstream.receive(subscription: self) - } - - func request(_ demand: Subscribers.Demand) { - guard demand > 0 else { return } - - self._state.lock() - guard var config = self._state.value.activeConfiguration, !config.didDownstreamRequestValues else { return self._state.unlock() } - config.didDownstreamRequestValues = true - self._state.value = .active(config) - self._state.unlock() - - config.upstream.request(self._maxDemand) - } - - func receive(_ input: Upstream.Output) -> Subscribers.Demand { - .max(1) - } - - func receive(completion: Subscribers.Completion) { - guard case .active(let config) = self._state.terminate() else { return } - config.downstream.receive(completion: completion) - } - - func cancel() { - guard case .active(let config) = self._state.terminate() else { return } - config.upstream.cancel() - } + /// Helper that acts as a `Subscriber` for the upstream, but just forward events to the given `Conduit` instance. + final class UpstreamConduit: Subscription, Subscriber where Downstream:Subscriber, Downstream.Input==Child.Output, Downstream.Failure==Child.Failure { + typealias Input = Upstream.Output + typealias Failure = Upstream.Failure + + /// Enum listing all possible conduit states. + @ConduitLock private var state: ConduitState<_WaitConfiguration,_ActiveConfiguration> + /// The combine identifier shared with the `DownstreamConduit`. + let combineIdentifier: CombineIdentifier + /// The maximum demand requested to the upstream at the same time. + private let _maxDemand: Subscribers.Demand + + /// Designated initializer for this helper establishing the strong bond between the `Conduit` and the created helper. + init(subscriber: _DownstreamConduit, maxDemand: Subscribers.Demand) { + precondition(maxDemand > .none) + self.state = .awaitingSubscription(_WaitConfiguration(downstream: subscriber)) + self.combineIdentifier = subscriber.combineIdentifier + self._maxDemand = maxDemand } -} -private extension Publishers.Then.UpstreamConduit { - /// The necessary variables during the *awaiting* stage. - /// - /// The *Conduit* has been initialized, but it is not yet connected to the upstream. - struct _WaitConfiguration { - let downstream: Publishers.Then.DownstreamConduit + deinit { + self.cancel() + self._state.invalidate() } - /// The necessary variables during the *active* stage. - /// - /// The *Conduit* is receiving values from upstream. - struct _ActiveConfiguration { - let upstream: Subscription - let downstream: Publishers.Then.DownstreamConduit - var didDownstreamRequestValues: Bool + + func receive(subscription: Subscription) { + guard let config = self._state.activate(atomic: { _ActiveConfiguration(upstream: subscription, downstream: $0.downstream, didDownstreamRequestValues: false) }) else { + return subscription.cancel() + } + config.downstream.receive(subscription: self) + } + + func request(_ demand: Subscribers.Demand) { + guard demand > 0 else { return } + + self._state.lock() + guard var config = self._state.value.activeConfiguration, !config.didDownstreamRequestValues else { return self._state.unlock() } + config.didDownstreamRequestValues = true + self._state.value = .active(config) + self._state.unlock() + + config.upstream.request(self._maxDemand) + } + + func receive(_ input: Upstream.Output) -> Subscribers.Demand { + .max(1) + } + + func receive(completion: Subscribers.Completion) { + guard case .active(let config) = self._state.terminate() else { return } + config.downstream.receive(completion: completion) + } + + func cancel() { + guard case .active(let config) = self._state.terminate() else { return } + config.upstream.cancel() } + } +} + +private extension Publishers.Then.UpstreamConduit { + /// The necessary variables during the *awaiting* stage. + /// + /// The *Conduit* has been initialized, but it is not yet connected to the upstream. + struct _WaitConfiguration { + let downstream: Publishers.Then._DownstreamConduit + } + /// The necessary variables during the *active* stage. + /// + /// The *Conduit* is receiving values from upstream. + struct _ActiveConfiguration { + let upstream: Subscription + let downstream: Publishers.Then._DownstreamConduit + var didDownstreamRequestValues: Bool + } } // MARK: - -fileprivate extension Publishers.Then { - /// Represents an active `Then` publisher taking both the role of `Subscriber` (for upstream publishers) and `Subscription` (for downstream subscribers). - /// - /// This subscriber takes as inputs any value provided from upstream, but ignores them. Only when a successful completion has been received, a `Child` publisher will get generated. - /// The child events will get emitted as-is (i.e. without any modification). - final class DownstreamConduit: Subscription, Subscriber where Downstream: Subscriber, Downstream.Input==Child.Output, Downstream.Failure==Child.Failure { - typealias Input = Downstream.Input - typealias Failure = Downstream.Failure - - /// Enum listing all possible conduit states. - @ConduitLock private var state: ConduitState<_WaitConfiguration,_ActiveConfiguration> - - /// Designated initializer holding the downstream subscribers. - /// - parameter downstream: The subscriber receiving values downstream. - /// - parameter transform: The closure that will eventually generate another publisher to switch to. - init(downstream: Downstream, transform: @escaping Closure) { - self.state = .awaitingSubscription(_WaitConfiguration(closure: transform, downstream: downstream)) - } - - deinit { - self.cancel() - self._state.invalidate() - } - - func receive(subscription: Subscription) { - // A subscription can be received in the following two cases: - // a) The pipeline has just started and an acknowledgment is being waited from the upstream. - // b) The upstream has completed successfully and a child has been instantiated. The acknowledgement is being waited upon. - self._state.lock() - switch self._state.value { - case .awaitingSubscription(let config): - self._state.value = .active(_ActiveConfiguration(stage: .upstream(subscription: subscription, closure: config.closure, storedRequests: .none), downstream: config.downstream)) - self._state.unlock() - config.downstream.receive(subscription: self) - case .active(var config): - guard case .awaitingChild(let storedRequests) = config.stage else { fatalError() } - config.stage = .child(subscription: subscription) - self._state.value = .active(config) - self._state.unlock() - subscription.request(storedRequests) - case .terminated: - self._state.unlock() - } - } - - func request(_ demand: Subscribers.Demand) { - guard demand > 0 else { return } - - self._state.lock() - guard var config = self._state.value.activeConfiguration else { return self._state.unlock() } - - switch config.stage { - case .upstream(let subscription, let closure, let requests): - config.stage = .upstream(subscription: subscription, closure: closure, storedRequests: requests + demand) - self._state.value = .active(config) - self._state.unlock() - if requests == .none { subscription.request(.max(1)) } - case .awaitingChild(let requests): - config.stage = .awaitingChild(storedRequests: requests + demand) - self._state.value = .active(config) - self._state.unlock() - case .child(let subscription): - self._state.unlock() - return subscription.request(demand) - } - } - - func receive(_ input: Downstream.Input) -> Subscribers.Demand { - self._state.lock() - guard let config = self._state.value.activeConfiguration else { self._state.unlock(); return .unlimited } - guard case .child = config.stage else { fatalError() } - self._state.unlock() - return config.downstream.receive(input) - } - - func receive(completion: Subscribers.Completion) { - self._state.lock() - guard var config = self._state.value.activeConfiguration else { return self._state.unlock() } - - switch config.stage { - case .upstream(_, let closure, let requests): - switch completion { - case .finished: - config.stage = .awaitingChild(storedRequests: requests) - self._state.value = .active(config) - self._state.unlock() - closure().subscribe(self) - case .failure: - self._state.value = .terminated - self._state.unlock() - config.downstream.receive(completion: completion) - } - case .awaitingChild: - fatalError() - case .child: - self._state.value = .terminated - self._state.unlock() - config.downstream.receive(completion: completion) - } - } +private extension Publishers.Then { + /// Represents an active `Then` publisher taking both the role of `Subscriber` (for upstream publishers) and `Subscription` (for downstream subscribers). + /// + /// This subscriber takes as inputs any value provided from upstream, but ignores them. Only when a successful completion has been received, a `Child` publisher will get generated. + /// The child events will get emitted as-is (i.e. without any modification). + final class _DownstreamConduit: Subscription, Subscriber where Downstream: Subscriber, Downstream.Input==Child.Output, Downstream.Failure==Child.Failure { + typealias Input = Downstream.Input + typealias Failure = Downstream.Failure - func cancel() { - guard case .active(let config) = self._state.terminate() else { return } - switch config.stage { - case .upstream(let subscription, _, _): subscription.cancel() - case .awaitingChild(_): break - case .child(let subscription): subscription.cancel() - } - } + /// Enum listing all possible conduit states. + @ConduitLock private var state: ConduitState<_WaitConfiguration,_ActiveConfiguration> + + /// Designated initializer holding the downstream subscribers. + /// - parameter downstream: The subscriber receiving values downstream. + /// - parameter transform: The closure that will eventually generate another publisher to switch to. + init(downstream: Downstream, transform: @escaping Closure) { + self.state = .awaitingSubscription(_WaitConfiguration(closure: transform, downstream: downstream)) } -} -private extension Publishers.Then.DownstreamConduit { - /// The necessary variables during the *awaiting* stage. - /// - /// The `Conduit` has been initialized, but it is not yet connected to the upstream. - struct _WaitConfiguration { - /// Closure generating the publisher which will take over once the publisher has completed (successfully). - let closure: Publishers.Then.Closure - /// The subscriber further down the chain. - let downstream: Downstream + deinit { + self.cancel() + self._state.invalidate() } - /// The necessary variables during the *active* stage. - /// - /// The *Conduit* is receiving values from upstream or child publisher. - struct _ActiveConfiguration { - /// The active stage - var stage: Stage - /// The subscriber further down the chain. - let downstream: Downstream - - /// Once the pipeline is activated, there are two main stages: upsatream connection, and child publishing. - enum Stage { - /// Values are being received from upstream, but the child publisher hasn't been activated (switched to) yet. - case upstream(subscription: Subscription, closure: ()->Child, storedRequests: Subscribers.Demand) - /// Upstream has completed successfully and the child publisher has been instantiated and it is being waited for subscription acknowledgement. - case awaitingChild(storedRequests: Subscribers.Demand) - /// Upstream has completed successfully and the child is sending values. - case child(subscription: Subscription) + + func receive(subscription: Subscription) { + // A subscription can be received in the following two cases: + // a) The pipeline has just started and an acknowledgment is being waited from the upstream. + // b) The upstream has completed successfully and a child has been instantiated. The acknowledgement is being waited upon. + self._state.lock() + switch self._state.value { + case .awaitingSubscription(let config): + self._state.value = .active(_ActiveConfiguration(stage: .upstream(subscription: subscription, closure: config.closure, storedRequests: .none), downstream: config.downstream)) + self._state.unlock() + config.downstream.receive(subscription: self) + case .active(var config): + guard case .awaitingChild(let storedRequests) = config.stage else { fatalError() } + config.stage = .child(subscription: subscription) + self._state.value = .active(config) + self._state.unlock() + subscription.request(storedRequests) + case .terminated: + self._state.unlock() + } + } + + func request(_ demand: Subscribers.Demand) { + guard demand > 0 else { return } + + self._state.lock() + guard var config = self._state.value.activeConfiguration else { return self._state.unlock() } + + switch config.stage { + case .upstream(let subscription, let closure, let requests): + config.stage = .upstream(subscription: subscription, closure: closure, storedRequests: requests + demand) + self._state.value = .active(config) + self._state.unlock() + if requests == .none { subscription.request(.max(1)) } + case .awaitingChild(let requests): + config.stage = .awaitingChild(storedRequests: requests + demand) + self._state.value = .active(config) + self._state.unlock() + case .child(let subscription): + self._state.unlock() + return subscription.request(demand) + } + } + + func receive(_ input: Downstream.Input) -> Subscribers.Demand { + self._state.lock() + guard let config = self._state.value.activeConfiguration else { self._state.unlock(); return .unlimited } + guard case .child = config.stage else { fatalError() } + self._state.unlock() + return config.downstream.receive(input) + } + + func receive(completion: Subscribers.Completion) { + self._state.lock() + guard var config = self._state.value.activeConfiguration else { return self._state.unlock() } + + switch config.stage { + case .upstream(_, let closure, let requests): + switch completion { + case .finished: + config.stage = .awaitingChild(storedRequests: requests) + self._state.value = .active(config) + self._state.unlock() + closure().subscribe(self) + case .failure: + self._state.value = .terminated + self._state.unlock() + config.downstream.receive(completion: completion) } + case .awaitingChild: + fatalError() + case .child: + self._state.value = .terminated + self._state.unlock() + config.downstream.receive(completion: completion) + } + } + + func cancel() { + guard case .active(let config) = self._state.terminate() else { return } + switch config.stage { + case .upstream(let subscription, _, _): subscription.cancel() + case .awaitingChild(_): break + case .child(let subscription): subscription.cancel() + } + } + } +} + +private extension Publishers.Then._DownstreamConduit { + /// The necessary variables during the *awaiting* stage. + /// + /// The `Conduit` has been initialized, but it is not yet connected to the upstream. + struct _WaitConfiguration { + /// Closure generating the publisher which will take over once the publisher has completed (successfully). + let closure: Publishers.Then.Closure + /// The subscriber further down the chain. + let downstream: Downstream + } + /// The necessary variables during the *active* stage. + /// + /// The *Conduit* is receiving values from upstream or child publisher. + struct _ActiveConfiguration { + /// The active stage + var stage: Stage + /// The subscriber further down the chain. + let downstream: Downstream + + /// Once the pipeline is activated, there are two main stages: upsatream connection, and child publishing. + enum Stage { + /// Values are being received from upstream, but the child publisher hasn't been activated (switched to) yet. + case upstream(subscription: Subscription, closure: ()->Child, storedRequests: Subscribers.Demand) + /// Upstream has completed successfully and the child publisher has been instantiated and it is being waited for subscription acknowledgement. + case awaitingChild(storedRequests: Subscribers.Demand) + /// Upstream has completed successfully and the child is sending values. + case child(subscription: Subscription) } + } } diff --git a/sources/runtime/subscribers/FixedSink.swift b/sources/runtime/subscribers/FixedSink.swift index 2245811..257d54a 100644 --- a/sources/runtime/subscribers/FixedSink.swift +++ b/sources/runtime/subscribers/FixedSink.swift @@ -1,94 +1,94 @@ import Combine extension Subscribers { - /// A subscriber that requests the given number of values upon subscription and then don't request any further. - /// - /// For example, if the subscriber is initialized with a demand of 5, this subscriber will received 0 to 5 values, but no more. - /// ```swift - /// let subscriber = FixedSink(demand: .max(5), receiveValue: { print($0) }) - /// ``` - /// If five values are received, then a successful completion is send to `receiveCompletion` and the upstream gets cancelled. - public final class FixedSink: Subscriber, Cancellable where Failure:Error { - /// The total allowed value events. - public let demand: Int - /// The closure executed when a value is received. - public private(set) var receiveValue: ((Input)->Void)? - /// The closure executed when a completion event is received. - public private(set) var receiveCompletion: ((Subscribers.Completion)->Void)? - /// The subscriber's state. - @ConduitLock private var state: ConduitState - - /// Designated initializer specifying the number of expected values. - /// - precondition: `demand` must be greater than zero. - /// - parameter demand: The maximum number of values to be received. - /// - parameter receiveCompletion: The closure executed when the provided amount of values are received or a completion event is received. - /// - parameter receiveValue: The closure executed when a value is received. - public init(demand: Int, receiveCompletion: ((Subscribers.Completion)->Void)? = nil, receiveValue: ((Input)->Void)? = nil) { - precondition(demand > 0) - self.demand = demand - self.receiveValue = receiveValue - self.receiveCompletion = receiveCompletion - self.state = .awaitingSubscription(()) - } - - deinit { - self.cancel() - self._state.invalidate() - } - - public func receive(subscription: Subscription) { - guard case .some = self._state.activate(atomic: { _ in .init(upstream: subscription, receivedValues: 0) }) else { - return subscription.cancel() - } - subscription.request(.max(self.demand)) - } - - public func receive(_ input: Input) -> Subscribers.Demand { - self._state.lock() - guard var config = self._state.value.activeConfiguration else { - self._state.unlock() - return .none - } - config.receivedValues += 1 - - if config.receivedValues < self.demand { - self._state.value = .active(config) - self._state.unlock() - self.receiveValue?(input) - } else { - self._state.value = .terminated - self._state.unlock() - self.receiveValue?(input) - self.receiveValue = nil - self.receiveCompletion?(.finished) - self.receiveCompletion = nil - config.upstream.cancel() - } - - return .none - } - - public func receive(completion: Subscribers.Completion) { - guard case .active = self._state.terminate() else { return } - self.receiveValue = nil - self.receiveCompletion?(completion) - self.receiveCompletion = nil - } - - public func cancel() { - guard case .active = self._state.terminate() else { return } - self.receiveValue = nil - self.receiveCompletion = nil - } + /// A subscriber that requests the given number of values upon subscription and then don't request any further. + /// + /// For example, if the subscriber is initialized with a demand of 5, this subscriber will received 0 to 5 values, but no more. + /// ```swift + /// let subscriber = FixedSink(demand: .max(5), receiveValue: { print($0) }) + /// ``` + /// If five values are received, then a successful completion is send to `receiveCompletion` and the upstream gets cancelled. + public final class FixedSink: Subscriber, Cancellable where Failure:Error { + /// The total allowed value events. + public let demand: Int + /// The closure executed when a value is received. + public private(set) var receiveValue: ((Input)->Void)? + /// The closure executed when a completion event is received. + public private(set) var receiveCompletion: ((Subscribers.Completion)->Void)? + /// The subscriber's state. + @ConduitLock private var state: ConduitState + + /// Designated initializer specifying the number of expected values. + /// - precondition: `demand` must be greater than zero. + /// - parameter demand: The maximum number of values to be received. + /// - parameter receiveCompletion: The closure executed when the provided amount of values are received or a completion event is received. + /// - parameter receiveValue: The closure executed when a value is received. + public init(demand: Int, receiveCompletion: ((Subscribers.Completion)->Void)? = nil, receiveValue: ((Input)->Void)? = nil) { + precondition(demand > 0) + self.demand = demand + self.receiveValue = receiveValue + self.receiveCompletion = receiveCompletion + self.state = .awaitingSubscription(()) + } + + deinit { + self.cancel() + self._state.invalidate() + } + + public func receive(subscription: Subscription) { + guard case .some = self._state.activate(atomic: { _ in .init(upstream: subscription, receivedValues: 0) }) else { + return subscription.cancel() + } + subscription.request(.max(self.demand)) + } + + public func receive(_ input: Input) -> Subscribers.Demand { + self._state.lock() + guard var config = self._state.value.activeConfiguration else { + self._state.unlock() + return .none + } + config.receivedValues += 1 + + if config.receivedValues < self.demand { + self._state.value = .active(config) + self._state.unlock() + self.receiveValue?(input) + } else { + self._state.value = .terminated + self._state.unlock() + self.receiveValue?(input) + self.receiveValue = nil + self.receiveCompletion?(.finished) + self.receiveCompletion = nil + config.upstream.cancel() + } + + return .none + } + + public func receive(completion: Subscribers.Completion) { + guard case .active = self._state.terminate() else { return } + self.receiveValue = nil + self.receiveCompletion?(completion) + self.receiveCompletion = nil } + + public func cancel() { + guard case .active = self._state.terminate() else { return } + self.receiveValue = nil + self.receiveCompletion = nil + } + } } private extension Subscribers.FixedSink { - /// Variables required during the *active* stage. - struct _Configuration { - /// Upstream subscription. - let upstream: Subscription - /// The current amount of values received. - var receivedValues: Int - } + /// Variables required during the *active* stage. + struct _Configuration { + /// Upstream subscription. + let upstream: Subscription + /// The current amount of values received. + var receivedValues: Int + } } diff --git a/sources/runtime/subscribers/GraduatedSink.swift b/sources/runtime/subscribers/GraduatedSink.swift index 280d64a..503b613 100644 --- a/sources/runtime/subscribers/GraduatedSink.swift +++ b/sources/runtime/subscribers/GraduatedSink.swift @@ -1,66 +1,66 @@ import Combine extension Subscribers { - /// A simple subscriber that requests the given number of values upon subscription, always maintaining the same demand. - public final class GraduatedSink: Subscriber, Cancellable where Failure:Error { - /// The maximum allowed in-flight events. - public let maxDemand: Subscribers.Demand - /// The closure executed when a value is received. - public private(set) var receiveValue: ((Input)->Void)? - /// The closure executed when a completion event is received. - public private(set) var receiveCompletion: ((Subscribers.Completion)->Void)? - /// The subscriber's state. - @ConduitLock private var state: ConduitState - - /// Designated initializer specifying the maximum in-flight events. - /// - precondition: `maxDemand` must be greater than zero. - /// - parameter maxDemand: The maximum allowed in-flight events. - /// - parameter receiveCompletion: The closure executed when a completion event is received. - /// - parameter receiveValue: The closure executed when a value is received. - public init(maxDemand: Subscribers.Demand, receiveCompletion: ((Subscribers.Completion)->Void)? = nil, receiveValue: ((Input)->Void)? = nil) { - precondition(maxDemand > 0) - self.maxDemand = maxDemand - self.receiveValue = receiveValue - self.receiveCompletion = receiveCompletion - self.state = .awaitingSubscription(()) - } - - deinit { - self.cancel() - self._state.invalidate() - } - - public func receive(subscription: Subscription) { - guard case .some = self._state.activate(atomic: { _ in .init(upstream: subscription) }) else { - return subscription.cancel() - } - subscription.request(self.maxDemand) - } - - public func receive(_ input: Input) -> Subscribers.Demand { - self.receiveValue?(input) - return .max(1) - } - - public func receive(completion: Subscribers.Completion) { - guard case .active = self._state.terminate() else { return } - self.receiveValue = nil - self.receiveCompletion?(completion) - self.receiveCompletion = nil - } - - public func cancel() { - guard case .active = self._state.terminate() else { return } - self.receiveValue = nil - self.receiveCompletion = nil - } + /// A simple subscriber that requests the given number of values upon subscription, always maintaining the same demand. + public final class GraduatedSink: Subscriber, Cancellable where Failure:Error { + /// The maximum allowed in-flight events. + public let maxDemand: Subscribers.Demand + /// The closure executed when a value is received. + public private(set) var receiveValue: ((Input)->Void)? + /// The closure executed when a completion event is received. + public private(set) var receiveCompletion: ((Subscribers.Completion)->Void)? + /// The subscriber's state. + @ConduitLock private var state: ConduitState + + /// Designated initializer specifying the maximum in-flight events. + /// - precondition: `maxDemand` must be greater than zero. + /// - parameter maxDemand: The maximum allowed in-flight events. + /// - parameter receiveCompletion: The closure executed when a completion event is received. + /// - parameter receiveValue: The closure executed when a value is received. + public init(maxDemand: Subscribers.Demand, receiveCompletion: ((Subscribers.Completion)->Void)? = nil, receiveValue: ((Input)->Void)? = nil) { + precondition(maxDemand > 0) + self.maxDemand = maxDemand + self.receiveValue = receiveValue + self.receiveCompletion = receiveCompletion + self.state = .awaitingSubscription(()) + } + + deinit { + self.cancel() + self._state.invalidate() + } + + public func receive(subscription: Subscription) { + guard case .some = self._state.activate(atomic: { _ in .init(upstream: subscription) }) else { + return subscription.cancel() + } + subscription.request(self.maxDemand) + } + + public func receive(_ input: Input) -> Subscribers.Demand { + self.receiveValue?(input) + return .max(1) + } + + public func receive(completion: Subscribers.Completion) { + guard case .active = self._state.terminate() else { return } + self.receiveValue = nil + self.receiveCompletion?(completion) + self.receiveCompletion = nil } + + public func cancel() { + guard case .active = self._state.terminate() else { return } + self.receiveValue = nil + self.receiveCompletion = nil + } + } } private extension Subscribers.GraduatedSink { - /// Variables required during the *active* stage. - struct _Configuration { - /// Upstream subscription. - let upstream: Subscription - } + /// Variables required during the *active* stage. + struct _Configuration { + /// Upstream subscription. + let upstream: Subscription + } } diff --git a/sources/runtime/utils/Buffer.swift b/sources/runtime/utils/Buffer.swift index 2c6ba0d..ca7256e 100644 --- a/sources/runtime/utils/Buffer.swift +++ b/sources/runtime/utils/Buffer.swift @@ -1,13 +1,13 @@ import Combine extension Publishers.BufferingStrategy { - /// Unconditionally prints a given message and stops execution when the buffer exhaust its capacity. - /// - parameter message: The string to print. The default is an empty string. - /// - parameter file: The file name to print with `message`. The default is the file where this function is called. - /// - parameter line: The line number to print along with `message`. The default is the line number where `fatalError()` - @_transparent public static func fatalError(_ message: @autoclosure @escaping () -> String = String(), file: StaticString = #file, line: UInt = #line) -> Self { - .customError({ - Swift.fatalError(message(), file: file, line: line) - }) - } + /// Unconditionally prints a given message and stops execution when the buffer exhaust its capacity. + /// - parameter message: The string to print. The default is an empty string. + /// - parameter file: The file name to print with `message`. The default is the file where this function is called. + /// - parameter line: The line number to print along with `message`. The default is the line number where `fatalError()` + @_transparent public static func fatalError(_ message: @autoclosure @escaping () -> String = String(), file: StaticString = #file, line: UInt = #line) -> Self { + .customError({ + Swift.fatalError(message(), file: file, line: line) + }) + } } diff --git a/sources/runtime/utils/Lock.swift b/sources/runtime/utils/Lock.swift index 8f4f073..ce201a7 100644 --- a/sources/runtime/utils/Lock.swift +++ b/sources/runtime/utils/Lock.swift @@ -4,82 +4,82 @@ import Darwin /// /// - attention: Always make sure to deinitialize the lock. @propertyWrapper public struct ConduitLock { - /// Performant non-reentrant unfair lock. - private var _lock: UnsafeMutablePointer - /// Generic variable being guarded by the lock. - public var value: Value - - public init(wrappedValue: Value) { - self._lock = UnsafeMutablePointer.allocate(capacity: 1) - self._lock.initialize(to: os_unfair_lock()) - self.value = wrappedValue + /// Performant non-reentrant unfair lock. + private var _lock: UnsafeMutablePointer + /// Generic variable being guarded by the lock. + public var value: Value + + public init(wrappedValue: Value) { + self._lock = UnsafeMutablePointer.allocate(capacity: 1) + self._lock.initialize(to: os_unfair_lock()) + self.value = wrappedValue + } + + /// Provide thread-safe storage access (within the lock). + public var wrappedValue: Value { + get { + self.lock() + let content = self.value + self.unlock() + return content } - - /// Provide thread-safe storage access (within the lock). - public var wrappedValue: Value { - get { - self.lock() - let content = self.value - self.unlock() - return content - } - set { - self.lock() - self.value = newValue - self.unlock() - } + set { + self.lock() + self.value = newValue + self.unlock() } + } } extension ConduitLock { - /// Locks the state to other threads. - public func lock() { - os_unfair_lock_lock(self._lock) - } - - /// Unlocks the state for other threads. - public func unlock() { - os_unfair_lock_unlock(self._lock) - } - - public func invalidate() { - self._lock.deinitialize(count: 1) - self._lock.deallocate() - } + /// Locks the state to other threads. + public func lock() { + os_unfair_lock_lock(self._lock) + } + + /// Unlocks the state for other threads. + public func unlock() { + os_unfair_lock_unlock(self._lock) + } + + public func invalidate() { + self._lock.deinitialize(count: 1) + self._lock.deallocate() + } } extension ConduitLock { - /// The type of the value being guarded by the lock. - public typealias Value = ConduitState + /// The type of the value being guarded by the lock. + public typealias Value = ConduitState - /// Switches the state from `.awaitingSubscription` to `.active` by providing the active configuration parameters. - /// - If the state is already in `.active`, this function crashes. - /// - If the state is `.terminated`, no work is performed. - /// - parameter atomic: Code executed within the unfair locks. Don't call anywhere here; just perform computations. - /// - returns: The active configuration set after the call of this function. - public mutating func activate(atomic: (WaitConfiguration)->ActiveConfiguration) -> ActiveConfiguration? { - let result: ActiveConfiguration? - - self.lock() - switch self.value { - case .awaitingSubscription(let awaitingConfiguration): - result = atomic(awaitingConfiguration) - self.value = .active(result.unsafelyUnwrapped) - case .terminated: result = nil - case .active: fatalError() - } - self.unlock() - - return result - } - - /// Nullify the state and returns the previous state value. - @discardableResult public mutating func terminate() -> Value { - self.lock() - let result = self.value - self.value = .terminated - self.unlock() - return result + /// Switches the state from `.awaitingSubscription` to `.active` by providing the active configuration parameters. + /// - If the state is already in `.active`, this function crashes. + /// - If the state is `.terminated`, no work is performed. + /// - parameter atomic: Code executed within the unfair locks. Don't call anywhere here; just perform computations. + /// - returns: The active configuration set after the call of this function. + public mutating func activate(atomic: (WaitConfiguration)->ActiveConfiguration) -> ActiveConfiguration? { + let result: ActiveConfiguration? + + self.lock() + switch self.value { + case .awaitingSubscription(let awaitingConfiguration): + result = atomic(awaitingConfiguration) + self.value = .active(result.unsafelyUnwrapped) + case .terminated: result = nil + case .active: fatalError() } + self.unlock() + + return result + } + + /// Nullify the state and returns the previous state value. + @discardableResult public mutating func terminate() -> Value { + self.lock() + let result = self.value + self.value = .terminated + self.unlock() + return result + } } diff --git a/sources/runtime/utils/State.swift b/sources/runtime/utils/State.swift index 16750ef..e2c059b 100644 --- a/sources/runtime/utils/State.swift +++ b/sources/runtime/utils/State.swift @@ -1,55 +1,55 @@ /// States where conduit can find itself into. @frozen public enum ConduitState: ExpressibleByNilLiteral { - /// A subscriber has been sent upstream, but a subscription acknowledgement hasn't been received yet. - case awaitingSubscription(WaitConfiguration) - /// The conduit is active and potentially receiving and sending events. - case active(ActiveConfiguration) - /// The conduit has been cancelled or it has been terminated. - case terminated - - public init(nilLiteral: ()) { - self = .terminated - } + /// A subscriber has been sent upstream, but a subscription acknowledgement hasn't been received yet. + case awaitingSubscription(WaitConfiguration) + /// The conduit is active and potentially receiving and sending events. + case active(ActiveConfiguration) + /// The conduit has been cancelled or it has been terminated. + case terminated + + public init(nilLiteral: ()) { + self = .terminated + } } extension ConduitState { - /// Returns the `WaitConfiguration` if the receiving state is at `.awaitingSubscription`. `nil` for `.terminated` states, and it produces a fatal error otherwise. - /// - /// It is used on places where `Combine` promises that a subscription might only be in `.awaitingSubscription` or `.terminated` state, but never on `.active`. - @_transparent public var awaitingConfiguration: WaitConfiguration? { - switch self { - case .awaitingSubscription(let config): return config - case .terminated: return nil - case .active: fatalError() - } + /// Returns the `WaitConfiguration` if the receiving state is at `.awaitingSubscription`. `nil` for `.terminated` states, and it produces a fatal error otherwise. + /// + /// It is used on places where `Combine` promises that a subscription might only be in `.awaitingSubscription` or `.terminated` state, but never on `.active`. + @_transparent public var awaitingConfiguration: WaitConfiguration? { + switch self { + case .awaitingSubscription(let config): return config + case .terminated: return nil + case .active: fatalError() } - - /// Returns the `ActiveConfiguration` if the receiving state is at `.active`. `nil` for `.terminated` states, and it produces a fatal error otherwise. - /// - /// It is used on places where `Combine` promises that a subscription might only be in `.active` or `.terminated` state, but never on `.awaitingSubscription`. - @_transparent public var activeConfiguration: ActiveConfiguration? { - switch self { - case .active(let config): return config - case .terminated: return nil - case .awaitingSubscription: fatalError() - } + } + + /// Returns the `ActiveConfiguration` if the receiving state is at `.active`. `nil` for `.terminated` states, and it produces a fatal error otherwise. + /// + /// It is used on places where `Combine` promises that a subscription might only be in `.active` or `.terminated` state, but never on `.awaitingSubscription`. + @_transparent public var activeConfiguration: ActiveConfiguration? { + switch self { + case .active(let config): return config + case .terminated: return nil + case .awaitingSubscription: fatalError() } + } } extension ConduitState { - /// Boolean indicating if the state is still active. - @_transparent public var isActive: Bool { - switch self { - case .active: return true - case .awaitingSubscription, .terminated: return false - } + /// Boolean indicating if the state is still active. + @_transparent public var isActive: Bool { + switch self { + case .active: return true + case .awaitingSubscription, .terminated: return false } - - /// Boolean indicating if the state has been terminated. - @_transparent public var isTerminated: Bool { - switch self { - case .terminated: return true - case .awaitingSubscription, .active: return false - } + } + + /// Boolean indicating if the state has been terminated. + @_transparent public var isTerminated: Bool { + switch self { + case .terminated: return true + case .awaitingSubscription, .active: return false } + } } diff --git a/sources/testing/Expectations.swift b/sources/testing/Expectations.swift index 6b6e1f3..582c4ea 100644 --- a/sources/testing/Expectations.swift +++ b/sources/testing/Expectations.swift @@ -3,176 +3,176 @@ import XCTest import Combine extension Publisher { - /// Expects the receiving publisher to complete (with or without values) within the provided timeout. - /// - /// This operator will subscribe to the publisher chain and "block" the running test till the expectation is completed or thet timeout ellapses. - /// - precondition: `timeout` must be greater than or equal to zero. - /// - parameter timeout: The maximum amount of seconds that the test will wait. It must be greater or equal to zero. - /// - parameter test: The test were the expectation shall be fulfilled. - /// - parameter description: The expectation description. - /// - parameter file: The file in which failure occurred. Defaults to the file name of the test case in which this function was called. - /// - parameter line: The line number on which failure occurred. Defaults to the line number on which this function was called. - public func expectsCompletion(timeout: TimeInterval, on test: XCTWaiterDelegate, _ description: String = "The publisher completes successfully", file: StaticString = #file, line: UInt = #line) { - precondition(timeout >= 0) - let exp = XCTestExpectation(description: description) - - let cancellable = self.sink(receiveCompletion: { - switch $0 { - case .finished: exp.fulfill() - case .failure(let e): XCTFail("The publisher completed with failure when successfull completion was expected.\n\(e)\n", file: (file), line: line) - } - }, receiveValue: { _ in return }) - - let waiter = XCTWaiter(delegate: test) - waiter.wait(for: [exp], timeout: timeout) - cancellable.cancel() - } - - /// Expects the receiving publisher to complete with a failure within the provided timeout. - /// - /// This operator will subscribe to the publisher chain and "block" the running test till the expectation is completed or thet timeout ellapses. - /// - precondition: `timeout` must be greater than or equal to zero. - /// - parameter timeout: The maximum amount of seconds that the test will wait. It must be greater or equal to zero. - /// - parameter test: The test were the expectation shall be fulfilled. - /// - parameter description: The expectation description. - /// - parameter file: The file in which failure occurred. Defaults to the file name of the test case in which this function was called. - /// - parameter line: The line number on which failure occurred. Defaults to the line number on which this function was called. - public func expectsFailure(timeout: TimeInterval, on test: XCTWaiterDelegate, _ description: String = "The publisher completes with failure", file: StaticString = #file, line: UInt = #line) { - precondition(timeout >= 0) - let exp = XCTestExpectation(description: description) - - let cancellable = self.sink(receiveCompletion: { - switch $0 { - case .finished: XCTFail("The publisher completed successfully when a failure was expected", file: (file), line: line) - case .failure(_): exp.fulfill() - } - }, receiveValue: { (_) in return }) - - let waiter = XCTWaiter(delegate: test) - waiter.wait(for: [exp], timeout: timeout) - cancellable.cancel() - } - - /// Expects the receiving publisher to produce a single value and then complete within the provided timeout. - /// - /// This operator will subscribe to the publisher chain and "block" the running test till the expectation is completed or thet timeout ellapses. - /// - precondition: `timeout` must be greater than or equal to zero. - /// - parameter timeout: The maximum amount of seconds that the test will wait. It must be greater or equal to zero. - /// - parameter test: The test were the expectation shall be fulfilled. - /// - parameter description: The expectation description. - /// - parameter file: The file in which failure occurred. Defaults to the file name of the test case in which this function was called. - /// - parameter line: The line number on which failure occurred. Defaults to the line number on which this function was called. - /// - returns: The value forwarded by the publisher. - @discardableResult public func expectsOne(timeout: TimeInterval, on test: XCTWaiterDelegate, _ description: String = "The publisher emits a single value and then completes successfully", file: StaticString = #file, line: UInt = #line) -> Self.Output { - precondition(timeout >= 0) - let exp = XCTestExpectation(description: description) - - var value: Self.Output? = nil - var cancellable: AnyCancellable? - cancellable = self.sink(receiveCompletion: { - cancellable = nil - switch $0 { - case .failure(let e): - return XCTFail("The publisher completed with failure when successfull completion was expected\n\(e)\n", file: (file), line: line) - case .finished: - guard case .some = value else { - return XCTFail("The publisher completed without outputting any value", file: (file), line: line) - } - exp.fulfill() - } - }, receiveValue: { - guard case .none = value else { - cancellable?.cancel() - cancellable = nil - return XCTFail("The publisher produced more than one value when only one was expected", file: (file), line: line) - } - value = $0 - }) - - let waiter = XCTWaiter(delegate: test) - waiter.wait(for: [exp], timeout: timeout) - cancellable?.cancel() - - guard let result = value else { - XCTFail("The publisher didn't produce any value before the timeout ellapsed", file: (file), line: line) - fatalError(file: file, line: line) + /// Expects the receiving publisher to complete (with or without values) within the provided timeout. + /// + /// This operator will subscribe to the publisher chain and "block" the running test till the expectation is completed or thet timeout ellapses. + /// - precondition: `timeout` must be greater than or equal to zero. + /// - parameter timeout: The maximum amount of seconds that the test will wait. It must be greater or equal to zero. + /// - parameter test: The test were the expectation shall be fulfilled. + /// - parameter description: The expectation description. + /// - parameter file: The file in which failure occurred. Defaults to the file name of the test case in which this function was called. + /// - parameter line: The line number on which failure occurred. Defaults to the line number on which this function was called. + public func expectsCompletion(timeout: TimeInterval, on test: XCTWaiterDelegate, _ description: String = "The publisher completes successfully", file: StaticString = #file, line: UInt = #line) { + precondition(timeout >= 0) + let exp = XCTestExpectation(description: description) + + let cancellable = self.sink(receiveCompletion: { + switch $0 { + case .finished: exp.fulfill() + case .failure(let e): XCTFail("The publisher completed with failure when successfull completion was expected.\n\(e)\n", file: (file), line: line) + } + }, receiveValue: { _ in return }) + + let waiter = XCTWaiter(delegate: test) + waiter.wait(for: [exp], timeout: timeout) + cancellable.cancel() + } + + /// Expects the receiving publisher to complete with a failure within the provided timeout. + /// + /// This operator will subscribe to the publisher chain and "block" the running test till the expectation is completed or thet timeout ellapses. + /// - precondition: `timeout` must be greater than or equal to zero. + /// - parameter timeout: The maximum amount of seconds that the test will wait. It must be greater or equal to zero. + /// - parameter test: The test were the expectation shall be fulfilled. + /// - parameter description: The expectation description. + /// - parameter file: The file in which failure occurred. Defaults to the file name of the test case in which this function was called. + /// - parameter line: The line number on which failure occurred. Defaults to the line number on which this function was called. + public func expectsFailure(timeout: TimeInterval, on test: XCTWaiterDelegate, _ description: String = "The publisher completes with failure", file: StaticString = #file, line: UInt = #line) { + precondition(timeout >= 0) + let exp = XCTestExpectation(description: description) + + let cancellable = self.sink(receiveCompletion: { + switch $0 { + case .finished: XCTFail("The publisher completed successfully when a failure was expected", file: (file), line: line) + case .failure(_): exp.fulfill() + } + }, receiveValue: { (_) in return }) + + let waiter = XCTWaiter(delegate: test) + waiter.wait(for: [exp], timeout: timeout) + cancellable.cancel() + } + + /// Expects the receiving publisher to produce a single value and then complete within the provided timeout. + /// + /// This operator will subscribe to the publisher chain and "block" the running test till the expectation is completed or thet timeout ellapses. + /// - precondition: `timeout` must be greater than or equal to zero. + /// - parameter timeout: The maximum amount of seconds that the test will wait. It must be greater or equal to zero. + /// - parameter test: The test were the expectation shall be fulfilled. + /// - parameter description: The expectation description. + /// - parameter file: The file in which failure occurred. Defaults to the file name of the test case in which this function was called. + /// - parameter line: The line number on which failure occurred. Defaults to the line number on which this function was called. + /// - returns: The value forwarded by the publisher. + @discardableResult public func expectsOne(timeout: TimeInterval, on test: XCTWaiterDelegate, _ description: String = "The publisher emits a single value and then completes successfully", file: StaticString = #file, line: UInt = #line) -> Self.Output { + precondition(timeout >= 0) + let exp = XCTestExpectation(description: description) + + var value: Self.Output? = nil + var cancellable: AnyCancellable? + cancellable = self.sink(receiveCompletion: { + cancellable = nil + switch $0 { + case .failure(let e): + return XCTFail("The publisher completed with failure when successfull completion was expected\n\(e)\n", file: (file), line: line) + case .finished: + guard case .some = value else { + return XCTFail("The publisher completed without outputting any value", file: (file), line: line) } - return result - } - - /// Expects the receiving publisher to produce zero, one, or many values and then complete within the provided timeout. - /// - /// This operator will subscribe to the publisher chain and "block" the running test till the expectation is completed or thet timeout ellapses. - /// - precondition: `timeout` must be greater than or equal to zero. - /// - parameter timeout: The maximum amount of seconds that the test will wait. It must be greater or equal to zero. - /// - parameter test: The test were the expectation shall be fulfilled. - /// - parameter description: The expectation description. - /// - parameter file: The file in which failure occurred. Defaults to the file name of the test case in which this function was called. - /// - parameter line: The line number on which failure occurred. Defaults to the line number on which this function was called. - /// - returns: The forwarded values by the publisher (it can be empty). - @discardableResult public func expectsAll(timeout: TimeInterval, on test: XCTWaiterDelegate, _ description: String = "The publisher emits zero or more value and then completes successfully", file: StaticString = #file, line: UInt = #line) -> [Self.Output] { - precondition(timeout >= 0) - let exp = XCTestExpectation(description: description) - - var result: [Self.Output] = [] - var cancellable: AnyCancellable? - cancellable = self.sink(receiveCompletion: { - cancellable = nil - switch $0 { - case .finished: - exp.fulfill() - case .failure(let e): - XCTFail("The publisher completed with failure when successfull completion was expected\n\(e)\n", file: (file), line: line) - fatalError() - } - }, receiveValue: { result.append($0) }) - - let waiter = XCTWaiter(delegate: test) - waiter.wait(for: [exp], timeout: timeout) + exp.fulfill() + } + }, receiveValue: { + guard case .none = value else { cancellable?.cancel() - return result - } - - /// Expects the receiving publisher to produce at least a given number of values. Once the publisher has produced the given amount of values, it will get cancel by this function. - /// - /// This operator will subscribe to the publisher chain and "block" the running test till the expectation is completed or thet timeout ellapses. - /// - precondition: `values` must be greater than zero. - /// - precondition: `timeout` must be greater than or equal to zero. - /// - parameter timeout: The maximum amount of seconds that the test will wait. It must be greater or equal to zero. - /// - parameter test: The test were the expectation shall be fulfilled. - /// - parameter description: The expectation description. - /// - parameter file: The file in which failure occurred. Defaults to the file name of the test case in which this function was called. - /// - parameter line: The line number on which failure occurred. Defaults to the line number on which this function was called. - /// - parameter check: The closure to be executed per value received. - /// - returns: An array of all the values forwarded by the publisher. - @discardableResult public func expectsAtLeast(values: Int, timeout: TimeInterval, on test: XCTWaiterDelegate, _ description: String = "The publisher emits at least the given amount of values and then completes successfully", file: StaticString = #file, line: UInt = #line, foreach check: ((Output)->Void)? = nil) -> [Self.Output] { - precondition(values > 0 && timeout >= 0) - - let exp = XCTestExpectation(description: "Waiting for \(values) values") - - var result: [Self.Output] = [] - var cancellable: AnyCancellable? - cancellable = self.sink(receiveCompletion: { - cancellable = nil - switch $0 { - case .finished: if result.count == values { return exp.fulfill() } - case .failure(let e): XCTFail(String(describing: e), file: (file), line: line) - } - }, receiveValue: { (output) in - guard result.count < values else { return } - result.append(output) - check?(output) - guard result.count == values else { return } - cancellable?.cancel() - cancellable = nil - exp.fulfill() - }) - - let waiter = XCTWaiter(delegate: test) - waiter.wait(for: [exp], timeout: timeout) - cancellable?.cancel() - return result + cancellable = nil + return XCTFail("The publisher produced more than one value when only one was expected", file: (file), line: line) + } + value = $0 + }) + + let waiter = XCTWaiter(delegate: test) + waiter.wait(for: [exp], timeout: timeout) + cancellable?.cancel() + + guard let result = value else { + XCTFail("The publisher didn't produce any value before the timeout ellapsed", file: (file), line: line) + fatalError(file: file, line: line) } + return result + } + + /// Expects the receiving publisher to produce zero, one, or many values and then complete within the provided timeout. + /// + /// This operator will subscribe to the publisher chain and "block" the running test till the expectation is completed or thet timeout ellapses. + /// - precondition: `timeout` must be greater than or equal to zero. + /// - parameter timeout: The maximum amount of seconds that the test will wait. It must be greater or equal to zero. + /// - parameter test: The test were the expectation shall be fulfilled. + /// - parameter description: The expectation description. + /// - parameter file: The file in which failure occurred. Defaults to the file name of the test case in which this function was called. + /// - parameter line: The line number on which failure occurred. Defaults to the line number on which this function was called. + /// - returns: The forwarded values by the publisher (it can be empty). + @discardableResult public func expectsAll(timeout: TimeInterval, on test: XCTWaiterDelegate, _ description: String = "The publisher emits zero or more value and then completes successfully", file: StaticString = #file, line: UInt = #line) -> [Self.Output] { + precondition(timeout >= 0) + let exp = XCTestExpectation(description: description) + + var result: [Self.Output] = [] + var cancellable: AnyCancellable? + cancellable = self.sink(receiveCompletion: { + cancellable = nil + switch $0 { + case .finished: + exp.fulfill() + case .failure(let e): + XCTFail("The publisher completed with failure when successfull completion was expected\n\(e)\n", file: (file), line: line) + fatalError() + } + }, receiveValue: { result.append($0) }) + + let waiter = XCTWaiter(delegate: test) + waiter.wait(for: [exp], timeout: timeout) + cancellable?.cancel() + return result + } + + /// Expects the receiving publisher to produce at least a given number of values. Once the publisher has produced the given amount of values, it will get cancel by this function. + /// + /// This operator will subscribe to the publisher chain and "block" the running test till the expectation is completed or thet timeout ellapses. + /// - precondition: `values` must be greater than zero. + /// - precondition: `timeout` must be greater than or equal to zero. + /// - parameter timeout: The maximum amount of seconds that the test will wait. It must be greater or equal to zero. + /// - parameter test: The test were the expectation shall be fulfilled. + /// - parameter description: The expectation description. + /// - parameter file: The file in which failure occurred. Defaults to the file name of the test case in which this function was called. + /// - parameter line: The line number on which failure occurred. Defaults to the line number on which this function was called. + /// - parameter check: The closure to be executed per value received. + /// - returns: An array of all the values forwarded by the publisher. + @discardableResult public func expectsAtLeast(values: Int, timeout: TimeInterval, on test: XCTWaiterDelegate, _ description: String = "The publisher emits at least the given amount of values and then completes successfully", file: StaticString = #file, line: UInt = #line, foreach check: ((Output)->Void)? = nil) -> [Self.Output] { + precondition(values > 0 && timeout >= 0) + + let exp = XCTestExpectation(description: "Waiting for \(values) values") + + var result: [Self.Output] = [] + var cancellable: AnyCancellable? + cancellable = self.sink(receiveCompletion: { + cancellable = nil + switch $0 { + case .finished: if result.count == values { return exp.fulfill() } + case .failure(let e): XCTFail(String(describing: e), file: (file), line: line) + } + }, receiveValue: { (output) in + guard result.count < values else { return } + result.append(output) + check?(output) + guard result.count == values else { return } + cancellable?.cancel() + cancellable = nil + exp.fulfill() + }) + + let waiter = XCTWaiter(delegate: test) + waiter.wait(for: [exp], timeout: timeout) + cancellable?.cancel() + return result + } } #endif diff --git a/sources/testing/XCTestCase.swift b/sources/testing/XCTestCase.swift index ca478d9..b63cd01 100644 --- a/sources/testing/XCTestCase.swift +++ b/sources/testing/XCTestCase.swift @@ -4,20 +4,20 @@ import Combine import Foundation extension XCTestCase { - /// Locks the receiving test for `interval` seconds. - /// - parameter interval: The number of seconds waiting (must be greater than zero). - public func wait(seconds interval: TimeInterval) { - precondition(interval > 0) - - let e = self.expectation(description: "Waiting for \(interval) seconds") - let timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: false) { - $0.invalidate() - e.fulfill() - } - - self.wait(for: [e], timeout: interval) - timer.invalidate() + /// Locks the receiving test for `interval` seconds. + /// - parameter interval: The number of seconds waiting (must be greater than zero). + public func wait(seconds interval: TimeInterval) { + precondition(interval > 0) + + let e = self.expectation(description: "Waiting for \(interval) seconds") + let timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: false) { + $0.invalidate() + e.fulfill() } + + self.wait(for: [e], timeout: interval) + timer.invalidate() + } } #endif diff --git a/tests/runtime/operators/AssignOpTests.swift b/tests/runtime/operators/AssignOpTests.swift index ad6c938..933562b 100644 --- a/tests/runtime/operators/AssignOpTests.swift +++ b/tests/runtime/operators/AssignOpTests.swift @@ -4,52 +4,56 @@ import Combine /// Tests the correct behavior of the `assign(to:on:)` and `invoke(_:on:)` operators. final class AssignOpTests: XCTestCase { - /// A convenience storage of cancellables. - private var _cancellables = Set() - - override func setUp() { - self.continueAfterFailure = false - self._cancellables.removeAll() - } + /// A convenience storage of cancellables. + private var _cancellables = Set() + + override func setUp() { + self.continueAfterFailure = false + self._cancellables.removeAll() + } + + override func tearDown() { + self._cancellables.removeAll() + } } extension AssignOpTests { - /// Tests the `assign(to:onWeak:)` operations. - func testRegularAssign() { - let data = [1, 2, 3, 4] - final class _Custom { var value: Int = 0 } - - let objA = _Custom() - data.publisher.assign(to: \.value, on: objA).store(in: &self._cancellables) - XCTAssert(objA.value == data.last!) - - let objB = _Custom() - data.publisher.assign(to: \.value, onWeak: objB).store(in: &self._cancellables) - XCTAssert(objB.value == data.last!) - - let objC = _Custom() - data.publisher.assign(to: \.value, onWeak: objC).store(in: &self._cancellables) - XCTAssert(objC.value == data.last!) - } - - /// Tests the `invoke(_:on:)` operations. - func testRegularInvocation() { - let data = [1, 2, 3, 4] - final class _Custom { - private(set) var value: Int = 0 - func setNumber(value: Int) -> Void { self.value = value } - } - - let objA = _Custom() - data.publisher.invoke(_Custom.setNumber, on: objA).store(in: &self._cancellables) - XCTAssert(objA.value == data.last!) - - let objB = _Custom() - data.publisher.invoke(_Custom.setNumber, onWeak: objB).store(in: &self._cancellables) - XCTAssert(objB.value == data.last!) - - let objC = _Custom() - data.publisher.invoke(_Custom.setNumber, onWeak: objC).store(in: &self._cancellables) - XCTAssert(objC.value == data.last!) + /// Tests the `assign(to:onWeak:)` operations. + func testRegularAssign() { + let data = [1, 2, 3, 4] + final class _Custom { var value: Int = 0 } + + let objA = _Custom() + data.publisher.assign(to: \.value, on: objA).store(in: &self._cancellables) + XCTAssert(objA.value == data.last!) + + let objB = _Custom() + data.publisher.assign(to: \.value, onWeak: objB).store(in: &self._cancellables) + XCTAssert(objB.value == data.last!) + + let objC = _Custom() + data.publisher.assign(to: \.value, onWeak: objC).store(in: &self._cancellables) + XCTAssert(objC.value == data.last!) + } + + /// Tests the `invoke(_:on:)` operations. + func testRegularInvocation() { + let data = [1, 2, 3, 4] + final class _Custom { + private(set) var value: Int = 0 + func setNumber(value: Int) -> Void { self.value = value } } + + let objA = _Custom() + data.publisher.invoke(_Custom.setNumber, on: objA).store(in: &self._cancellables) + XCTAssert(objA.value == data.last!) + + let objB = _Custom() + data.publisher.invoke(_Custom.setNumber, onWeak: objB).store(in: &self._cancellables) + XCTAssert(objB.value == data.last!) + + let objC = _Custom() + data.publisher.invoke(_Custom.setNumber, onWeak: objC).store(in: &self._cancellables) + XCTAssert(objC.value == data.last!) + } } diff --git a/tests/runtime/operators/AwaitOpTests.swift b/tests/runtime/operators/AwaitOpTests.swift index 1171395..2db4d31 100644 --- a/tests/runtime/operators/AwaitOpTests.swift +++ b/tests/runtime/operators/AwaitOpTests.swift @@ -4,25 +4,25 @@ import Combine /// Tests the correct behavior of the `await` operator. final class AwaitOpTests: XCTestCase { - override func setUp() { - self.continueAfterFailure = false - } + override func setUp() { + self.continueAfterFailure = false + } } extension AwaitOpTests { - /// Tests the `await` operator. - func testAwait() { - let publisher = Just("Hello") - .delay(for: 1, scheduler: DispatchQueue.global()) - - let queue = DispatchQueue(label: "io.dehesa.conbini.tests.await") - let cancellable = Just(()) - .delay(for: 10, scheduler: queue) - .sink { XCTFail("The await test failed") } - - let greeting = publisher.await - XCTAssertEqual(greeting, "Hello") - - cancellable.cancel() - } + /// Tests the `await` operator. + func testAwait() { + let publisher = Just("Hello") + .delay(for: 1, scheduler: DispatchQueue.global()) + + let queue = DispatchQueue(label: "io.dehesa.conbini.tests.await") + let cancellable = Just(()) + .delay(for: 10, scheduler: queue) + .sink { XCTFail("The await test failed") } + + let greeting = publisher.await + XCTAssertEqual(greeting, "Hello") + + cancellable.cancel() + } } diff --git a/tests/runtime/operators/DelayedRetryOpTests.swift b/tests/runtime/operators/DelayedRetryOpTests.swift index c189685..ee92975 100644 --- a/tests/runtime/operators/DelayedRetryOpTests.swift +++ b/tests/runtime/operators/DelayedRetryOpTests.swift @@ -4,99 +4,99 @@ import Combine /// Tests the correct behavior of the `Then` operator. final class DelayedRetryOpTests: XCTestCase { - /// A custom error to send as a dummy. - private struct CustomError: Swift.Error {} - - override func setUp() { - self.continueAfterFailure = false - } + override func setUp() { + self.continueAfterFailure = false + } + + /// A custom error to send as a dummy. + private struct _CustomError: Swift.Error {} } extension DelayedRetryOpTests { - /// Test a normal "happy path" example for the custom "delayed retry" operator. - func testSuccessfulEnding() { - let e = self.expectation(description: "Successful completion") - - let input = 0..<10 - var output = [Int]() - let queue = DispatchQueue(label: "io.dehesa.conbini.tests.operators.retry") - - let cancellable = input.publisher - .map { $0 * 2 } - .retry(on: queue, intervals: [0, 0.2, 0.5]) - .map { $0 * 2 } - .sink(receiveCompletion: { - guard case .finished = $0 else { return XCTFail("A failure completion has been received, when a successful one was expected") } - e.fulfill() - }, receiveValue: { output.append($0) }) - - self.wait(for: [e], timeout: 0.2) - XCTAssertEqual(Array(input).map { $0 * 4 }, output) - cancellable.cancel() - } - - /// Tests a failure reception and successful recovery. - func testSingleFailureRecovery() { - let e = self.expectation(description: "Single failure recovery") - - let input = [0, 1, 2] - var output = [Int]() - var passes = 0 - var marker: (start: CFAbsoluteTime?, end: CFAbsoluteTime?) = (nil, nil) - let queue = DispatchQueue(label: "io.dehesa.conbini.tests.operators.retry") - - let cancellable = DeferredPassthrough { (subject) in - passes += 1 - if passes == 2 { marker.end = CFAbsoluteTimeGetCurrent() } - - subject.send(input[0]) - subject.send(input[1]) - subject.send(input[2]) - - if passes == 1 { - marker.start = CFAbsoluteTimeGetCurrent() - subject.send(completion: .failure(CustomError())) - } else { - subject.send(completion: .finished) - } - }.retry(on: queue, intervals: [0.2, 0.4, 0.6]) - .sink(receiveCompletion: { - guard case .finished = $0 else { return XCTFail("A failure completion has been received, when a successful one was expected") } - e.fulfill() - }, receiveValue: { output.append($0) }) - - self.wait(for: [e], timeout: 0.6) - XCTAssertEqual(passes, 2) - XCTAssertEqual(input + input, output) - XCTAssertGreaterThan(marker.end! - marker.start!, 0.2) - cancellable.cancel() - } - - /// Test publishing a stream of failure from which is impossible to recover. - func testFailuresStream() { - let e = self.expectation(description: "Failure stream") - - let intervals: [TimeInterval] = [0.1, 0.3, 0.5] - var markers = [CFAbsoluteTime]() - let queue = DispatchQueue(label: "io.dehesa.conbini.tests.operators.retry") - - let cancellable = Deferred { Fail(outputType: Int.self, failure: CustomError()) } - .handleEvents(receiveCompletion: { - markers.append(CFAbsoluteTimeGetCurrent()) - guard case .failure = $0 else { return XCTFail("A success completion has been received, when a failure one was expected") } - }) - .retry(on: queue, intervals: intervals) - .sink(receiveCompletion: { - markers.append(CFAbsoluteTimeGetCurrent()) - guard case .failure = $0 else { return XCTFail("A success completion has been received, when a failure one was expected") } - e.fulfill() - }, receiveValue: { _ in XCTFail("A value has been received when none were expected") }) - - self.wait(for: [e], timeout: 3) - XCTAssertEqual(markers.count, 5) - XCTAssertGreaterThan(markers[1] - intervals[0], intervals[0]) - XCTAssertGreaterThan(markers[2] - intervals[1], intervals[1]) - XCTAssertGreaterThan(markers[3] - intervals[2], intervals[2]) - cancellable.cancel() - } + /// Test a normal "happy path" example for the custom "delayed retry" operator. + func testSuccessfulEnding() { + let e = self.expectation(description: "Successful completion") + + let input = 0..<10 + var output = [Int]() + let queue = DispatchQueue(label: "io.dehesa.conbini.tests.operators.retry") + + let cancellable = input.publisher + .map { $0 * 2 } + .retry(on: queue, intervals: [0, 0.2, 0.5]) + .map { $0 * 2 } + .sink(receiveCompletion: { + guard case .finished = $0 else { return XCTFail("A failure completion has been received, when a successful one was expected") } + e.fulfill() + }, receiveValue: { output.append($0) }) + + self.wait(for: [e], timeout: 0.2) + XCTAssertEqual(Array(input).map { $0 * 4 }, output) + cancellable.cancel() + } + + /// Tests a failure reception and successful recovery. + func testSingleFailureRecovery() { + let e = self.expectation(description: "Single failure recovery") + + let input = [0, 1, 2] + var output = [Int]() + var passes = 0 + var marker: (start: CFAbsoluteTime?, end: CFAbsoluteTime?) = (nil, nil) + let queue = DispatchQueue(label: "io.dehesa.conbini.tests.operators.retry") + + let cancellable = DeferredPassthrough { (subject) in + passes += 1 + if passes == 2 { marker.end = CFAbsoluteTimeGetCurrent() } + + subject.send(input[0]) + subject.send(input[1]) + subject.send(input[2]) + + if passes == 1 { + marker.start = CFAbsoluteTimeGetCurrent() + subject.send(completion: .failure(_CustomError())) + } else { + subject.send(completion: .finished) + } + }.retry(on: queue, intervals: [0.2, 0.4, 0.6]) + .sink(receiveCompletion: { + guard case .finished = $0 else { return XCTFail("A failure completion has been received, when a successful one was expected") } + e.fulfill() + }, receiveValue: { output.append($0) }) + + self.wait(for: [e], timeout: 0.6) + XCTAssertEqual(passes, 2) + XCTAssertEqual(input + input, output) + XCTAssertGreaterThan(marker.end! - marker.start!, 0.2) + cancellable.cancel() + } + + /// Test publishing a stream of failure from which is impossible to recover. + func testFailuresStream() { + let e = self.expectation(description: "Failure stream") + + let intervals: [TimeInterval] = [0.1, 0.3, 0.5] + var markers = [CFAbsoluteTime]() + let queue = DispatchQueue(label: "io.dehesa.conbini.tests.operators.retry") + + let cancellable = Deferred { Fail(outputType: Int.self, failure: _CustomError()) } + .handleEvents(receiveCompletion: { + markers.append(CFAbsoluteTimeGetCurrent()) + guard case .failure = $0 else { return XCTFail("A success completion has been received, when a failure one was expected") } + }) + .retry(on: queue, intervals: intervals) + .sink(receiveCompletion: { + markers.append(CFAbsoluteTimeGetCurrent()) + guard case .failure = $0 else { return XCTFail("A success completion has been received, when a failure one was expected") } + e.fulfill() + }, receiveValue: { _ in XCTFail("A value has been received when none were expected") }) + + self.wait(for: [e], timeout: 3) + XCTAssertEqual(markers.count, 5) + XCTAssertGreaterThan(markers[1] - intervals[0], intervals[0]) + XCTAssertGreaterThan(markers[2] - intervals[1], intervals[1]) + XCTAssertGreaterThan(markers[3] - intervals[2], intervals[2]) + cancellable.cancel() + } } diff --git a/tests/runtime/operators/HandleEndOpTests.swift b/tests/runtime/operators/HandleEndOpTests.swift index 11c980c..20422b9 100644 --- a/tests/runtime/operators/HandleEndOpTests.swift +++ b/tests/runtime/operators/HandleEndOpTests.swift @@ -4,130 +4,130 @@ import Combine /// Tests the correct behavior of the `Then` operator. final class HandleEndOpTests: XCTestCase { - /// A custom error to send as a dummy. - private struct CustomError: Swift.Error {} - - override func setUp() { - self.continueAfterFailure = false - } + override func setUp() { + self.continueAfterFailure = false + } + + /// A custom error to send as a dummy. + private struct _CustomError: Swift.Error {} } extension HandleEndOpTests { - /// Test a normal "happy path" example for the custom "then" combine operator. - func testSuccessfulEnding() { - let e = self.expectation(description: "Successful completion") - - let values = [0, 1, 2] - var (received, isFinished) = ([Int](), false) - - let subject = PassthroughSubject() - let cancellable = subject - .handleEnd { - guard !isFinished else { return XCTFail("The end closure has been executed more than once") } - - switch $0 { - case .none: XCTFail("A cancel event has been received, when a successful completion was expected") - case .finished: isFinished = true - case .failure: XCTFail("A failure completion has been received, when a successful one was expected") - } - }.sink(receiveCompletion: { _ in e.fulfill() }) { received.append($0) } - - let queue = DispatchQueue.main - queue.asyncAfter(deadline: .now() + .milliseconds(100)) { subject.send(values[0]) } - queue.asyncAfter(deadline: .now() + .milliseconds(200)) { subject.send(values[1]) } - queue.asyncAfter(deadline: .now() + .milliseconds(300)) { subject.send(completion: .finished) } - queue.asyncAfter(deadline: .now() + .milliseconds(400)) { subject.send(values[2]) } - - self.wait(for: [e], timeout: 1) - - XCTAssertTrue(isFinished) - XCTAssertEqual(received, values.dropLast()) - cancellable.cancel() - } - - /// Tests the behavior of the `then` operator reacting to a upstream error. - func testFailureEnding() { - let e = self.expectation(description: "Failure completion") - - let values = [0, 1, 2] - var (received, isFinished) = ([Int](), false) - - let subject = PassthroughSubject() - let cancellable = subject - .handleEnd { - guard !isFinished else { return XCTFail("The end closure has been executed more than once") } - - switch $0 { - case .none: XCTFail("A cancel event has been received, when a failure completion was expected") - case .finished: XCTFail("A successful completion has been received, when a failure one was expected") - case .failure: isFinished = true - } - }.sink(receiveCompletion: { _ in e.fulfill() }) { received.append($0) } - - let queue = DispatchQueue.main - queue.asyncAfter(deadline: .now() + .milliseconds(100)) { subject.send(values[0]) } - queue.asyncAfter(deadline: .now() + .milliseconds(200)) { subject.send(values[1]) } - queue.asyncAfter(deadline: .now() + .milliseconds(300)) { subject.send(completion: .failure(CustomError())) } - queue.asyncAfter(deadline: .now() + .milliseconds(400)) { subject.send(values[2]) } - - self.wait(for: [e], timeout: 1) - - XCTAssertTrue(isFinished) - XCTAssertEqual(received, values.dropLast()) - cancellable.cancel() - } - - func testCancelEnding() { - let eClosure = self.expectation(description: "Cancel completion on closure") - let eTimeout = self.expectation(description: "Cancel completion on timeout") - - let values = [0, 1] - var (received, isFinished) = ([Int](), false) - - let subject = PassthroughSubject() - let cancellable = subject - .handleEnd { - guard !isFinished else { return XCTFail("The end closure has been executed more than once") } - - switch $0 { - case .none: isFinished = true - case .finished: XCTFail("A successful completion has been received, when a cancellation was expected") - case .failure: XCTFail("A failure completion has been received, when a cancellation was expected") - } - - eClosure.fulfill() - }.sink(receiveCompletion: { _ in XCTFail() }) { received.append($0) } - - let queue = DispatchQueue.main - queue.asyncAfter(deadline: .now() + .milliseconds(100)) { subject.send(values[0]) } - queue.asyncAfter(deadline: .now() + .milliseconds(200)) { subject.send(values[1]) } - queue.asyncAfter(deadline: .now() + .milliseconds(300)) { cancellable.cancel() } - queue.asyncAfter(deadline: .now() + .milliseconds(400)) { eTimeout.fulfill() } - - self.wait(for: [eClosure, eTimeout], timeout: 1) - - XCTAssertTrue(isFinished) - XCTAssertEqual(received, values) - cancellable.cancel() - } - - func testAbruptEndings() { - let e = self.expectation(description: "Abrupt completion") - - var isFinished = false - let cancellable = Empty(completeImmediately: true) - .handleEnd { - guard !isFinished else { return XCTFail("The end closure has been executed more than once") } - - switch $0 { - case .none: XCTFail("A cancel event has been received, when a successful completion was expected") - case .finished: isFinished = true - case .failure: XCTFail("A failure completion has been received, when a successful one was expected") - } - }.sink(receiveCompletion: { (_) in e.fulfill() }, receiveValue: { _ in }) - - self.wait(for: [e], timeout: 0.5) - XCTAssertTrue(isFinished) - cancellable.cancel() - } + /// Test a normal "happy path" example for the custom "then" combine operator. + func testSuccessfulEnding() { + let e = self.expectation(description: "Successful completion") + + let values = [0, 1, 2] + var (received, isFinished) = ([Int](), false) + + let subject = PassthroughSubject() + let cancellable = subject + .handleEnd { + guard !isFinished else { return XCTFail("The end closure has been executed more than once") } + + switch $0 { + case .none: XCTFail("A cancel event has been received, when a successful completion was expected") + case .finished: isFinished = true + case .failure: XCTFail("A failure completion has been received, when a successful one was expected") + } + }.sink(receiveCompletion: { _ in e.fulfill() }) { received.append($0) } + + let queue = DispatchQueue.main + queue.asyncAfter(deadline: .now() + .milliseconds(100)) { subject.send(values[0]) } + queue.asyncAfter(deadline: .now() + .milliseconds(200)) { subject.send(values[1]) } + queue.asyncAfter(deadline: .now() + .milliseconds(300)) { subject.send(completion: .finished) } + queue.asyncAfter(deadline: .now() + .milliseconds(400)) { subject.send(values[2]) } + + self.wait(for: [e], timeout: 1) + + XCTAssertTrue(isFinished) + XCTAssertEqual(received, values.dropLast()) + cancellable.cancel() + } + + /// Tests the behavior of the `then` operator reacting to a upstream error. + func testFailureEnding() { + let e = self.expectation(description: "Failure completion") + + let values = [0, 1, 2] + var (received, isFinished) = ([Int](), false) + + let subject = PassthroughSubject() + let cancellable = subject + .handleEnd { + guard !isFinished else { return XCTFail("The end closure has been executed more than once") } + + switch $0 { + case .none: XCTFail("A cancel event has been received, when a failure completion was expected") + case .finished: XCTFail("A successful completion has been received, when a failure one was expected") + case .failure: isFinished = true + } + }.sink(receiveCompletion: { _ in e.fulfill() }) { received.append($0) } + + let queue = DispatchQueue.main + queue.asyncAfter(deadline: .now() + .milliseconds(100)) { subject.send(values[0]) } + queue.asyncAfter(deadline: .now() + .milliseconds(200)) { subject.send(values[1]) } + queue.asyncAfter(deadline: .now() + .milliseconds(300)) { subject.send(completion: .failure(_CustomError())) } + queue.asyncAfter(deadline: .now() + .milliseconds(400)) { subject.send(values[2]) } + + self.wait(for: [e], timeout: 1) + + XCTAssertTrue(isFinished) + XCTAssertEqual(received, values.dropLast()) + cancellable.cancel() + } + + func testCancelEnding() { + let eClosure = self.expectation(description: "Cancel completion on closure") + let eTimeout = self.expectation(description: "Cancel completion on timeout") + + let values = [0, 1] + var (received, isFinished) = ([Int](), false) + + let subject = PassthroughSubject() + let cancellable = subject + .handleEnd { + guard !isFinished else { return XCTFail("The end closure has been executed more than once") } + + switch $0 { + case .none: isFinished = true + case .finished: XCTFail("A successful completion has been received, when a cancellation was expected") + case .failure: XCTFail("A failure completion has been received, when a cancellation was expected") + } + + eClosure.fulfill() + }.sink(receiveCompletion: { _ in XCTFail() }) { received.append($0) } + + let queue = DispatchQueue.main + queue.asyncAfter(deadline: .now() + .milliseconds(100)) { subject.send(values[0]) } + queue.asyncAfter(deadline: .now() + .milliseconds(200)) { subject.send(values[1]) } + queue.asyncAfter(deadline: .now() + .milliseconds(300)) { cancellable.cancel() } + queue.asyncAfter(deadline: .now() + .milliseconds(400)) { eTimeout.fulfill() } + + self.wait(for: [eClosure, eTimeout], timeout: 1) + + XCTAssertTrue(isFinished) + XCTAssertEqual(received, values) + cancellable.cancel() + } + + func testAbruptEndings() { + let e = self.expectation(description: "Abrupt completion") + + var isFinished = false + let cancellable = Empty(completeImmediately: true) + .handleEnd { + guard !isFinished else { return XCTFail("The end closure has been executed more than once") } + + switch $0 { + case .none: XCTFail("A cancel event has been received, when a successful completion was expected") + case .finished: isFinished = true + case .failure: XCTFail("A failure completion has been received, when a successful one was expected") + } + }.sink(receiveCompletion: { (_) in e.fulfill() }, receiveValue: { _ in }) + + self.wait(for: [e], timeout: 0.5) + XCTAssertTrue(isFinished) + cancellable.cancel() + } } diff --git a/tests/runtime/operators/ResultOpTests.swift b/tests/runtime/operators/ResultOpTests.swift index 76e507c..daa2f4a 100644 --- a/tests/runtime/operators/ResultOpTests.swift +++ b/tests/runtime/operators/ResultOpTests.swift @@ -4,72 +4,77 @@ import Combine /// Tests the correct behavior of the `result(onEmpty:)` operator. final class ResultOpTests: XCTestCase { - /// A custom error to send as a dummy. - private struct CustomError: Swift.Error {} - /// A convenience storage of cancellables. - private var _cancellables: Set = .init() - - override func setUp() { - self.continueAfterFailure = false - self._cancellables = .init() - } + /// A convenience storage of cancellables. + private var _cancellables = Set() + + override func setUp() { + self.continueAfterFailure = false + self._cancellables = .init() + } + + override func tearDown() { + self._cancellables.removeAll() + } + + /// A custom error to send as a dummy. + private struct _CustomError: Swift.Error {} } extension ResultOpTests { - /// Test the `result` operator with completions and no values. - func testCompletionWithoutValue() { - Empty(completeImmediately: true).result { _ in - XCTFail("The handler has been called although it was not expected to") - }.store(in: &self._cancellables) - - Empty(completeImmediately: false).result { _ in - XCTFail("The handler has been called although it was not expected to") - }.store(in: &self._cancellables) - } - - /// Tests the `result` operator with one value and completion. - func testRegularUsage() { - let input = 9 - - Just(input).result { - guard case .success(let received) = $0 else { return XCTFail() } - XCTAssertEqual(received, input) - }.store(in: &self._cancellables) - - [input].publisher.result { - guard case .success(let received) = $0 else { return XCTFail() } - XCTAssertEqual(received, input) - }.store(in: &self._cancellables) - - let exp = self.expectation(description: "Deferred passthrough provides a result") - DeferredPassthrough { (subject) in - let queue = DispatchQueue.main - queue.asyncAfter(deadline: .now() + .milliseconds(50)) { subject.send(input) } - queue.asyncAfter(deadline: .now() + .milliseconds(100)) { subject.send(completion: .finished) } - }.result { - guard case .success(let received) = $0 else { return XCTFail() } - XCTAssertEqual(received, input) - exp.fulfill() - }.store(in: &self._cancellables) - - self.wait(for: [exp], timeout: 0.2) - } - - /// Tests the `result` operator in failure situations. - func testFailure() { - Fail(error: .init()).result { - guard case .failure(_) = $0 else { return XCTFail() } - } - - let expA = self.expectation(description: "A failure result is provided") - DeferredPassthrough { (subject) in - let queue = DispatchQueue.main - queue.asyncAfter(deadline: .now() + .milliseconds(50)) { subject.send(completion: .failure(.init())) } - }.result { - guard case .failure(_) = $0 else { return XCTFail() } - expA.fulfill() - }.store(in: &self._cancellables) - - self.wait(for: [expA], timeout: 0.2) + /// Test the `result` operator with completions and no values. + func testCompletionWithoutValue() { + Empty(completeImmediately: true).result { _ in + XCTFail("The handler has been called although it was not expected to") + }.store(in: &self._cancellables) + + Empty(completeImmediately: false).result { _ in + XCTFail("The handler has been called although it was not expected to") + }.store(in: &self._cancellables) + } + + /// Tests the `result` operator with one value and completion. + func testRegularUsage() { + let input = 9 + + Just(input).result { + guard case .success(let received) = $0 else { return XCTFail() } + XCTAssertEqual(received, input) + }.store(in: &self._cancellables) + + [input].publisher.result { + guard case .success(let received) = $0 else { return XCTFail() } + XCTAssertEqual(received, input) + }.store(in: &self._cancellables) + + let exp = self.expectation(description: "Deferred passthrough provides a result") + DeferredPassthrough { (subject) in + let queue = DispatchQueue.main + queue.asyncAfter(deadline: .now() + .milliseconds(50)) { subject.send(input) } + queue.asyncAfter(deadline: .now() + .milliseconds(100)) { subject.send(completion: .finished) } + }.result { + guard case .success(let received) = $0 else { return XCTFail() } + XCTAssertEqual(received, input) + exp.fulfill() + }.store(in: &self._cancellables) + + self.wait(for: [exp], timeout: 0.2) + } + + /// Tests the `result` operator in failure situations. + func testFailure() { + Fail(error: .init()).result { + guard case .failure(_) = $0 else { return XCTFail() } } + + let expA = self.expectation(description: "A failure result is provided") + DeferredPassthrough { (subject) in + let queue = DispatchQueue.main + queue.asyncAfter(deadline: .now() + .milliseconds(50)) { subject.send(completion: .failure(.init())) } + }.result { + guard case .failure(_) = $0 else { return XCTFail() } + expA.fulfill() + }.store(in: &self._cancellables) + + self.wait(for: [expA], timeout: 0.2) + } } diff --git a/tests/runtime/operators/ThenOpTests.swift b/tests/runtime/operators/ThenOpTests.swift index 863049c..d7bf8e6 100644 --- a/tests/runtime/operators/ThenOpTests.swift +++ b/tests/runtime/operators/ThenOpTests.swift @@ -4,53 +4,53 @@ import Combine /// Tests the correct behavior of the `Then` operator. final class ThenOpTests: XCTestCase { - /// A custom error to send as a dummy. - private struct CustomError: Swift.Error {} - - override func setUp() { - self.continueAfterFailure = false - } + override func setUp() { + self.continueAfterFailure = false + } + + /// A custom error to send as a dummy. + private struct _CustomError: Swift.Error {} } extension ThenOpTests { - /// Test a normal "happy path" example for the custom "then" combine operator. - func testThenPassthrough() { - let e = self.expectation(description: "Successful completion") - let queue = DispatchQueue.main - - let subject = PassthroughSubject() - let cancellable = subject.then { - ["A", "B", "C"].publisher.setFailureType(to: CustomError.self) - }.sink(receiveCompletion: { _ in e.fulfill() }) { _ in return } - - queue.asyncAfter(deadline: .now() + .milliseconds(100)) { subject.send(0) } - queue.asyncAfter(deadline: .now() + .milliseconds(200)) { subject.send(1) } - queue.asyncAfter(deadline: .now() + .milliseconds(300)) { subject.send(2) } - queue.asyncAfter(deadline: .now() + .milliseconds(400)) { subject.send(completion: .finished) } - - self.wait(for: [e], timeout: 1) - cancellable.cancel() - } - - /// Tests the behavior of the `then` operator reacting to a upstream error. - func testThenFailure() { - let e = self.expectation(description: "Failure on origin") - let queue = DispatchQueue.main - - let subject = PassthroughSubject() - let cancellable = subject.then { - Future { (promise) in - queue.asyncAfter(deadline: .now() + .milliseconds(200)) { promise(.success("Completed")) } - } - }.sink(receiveCompletion: { (completion) in - guard case .failure = completion else { return } - e.fulfill() - }) { _ in return } - - queue.asyncAfter(deadline: .now() + .milliseconds(50)) { subject.send(0) } - queue.asyncAfter(deadline: .now() + .milliseconds(100)) { subject.send(completion: .failure(CustomError())) } - - self.wait(for: [e], timeout: 2) - cancellable.cancel() - } + /// Test a normal "happy path" example for the custom "then" combine operator. + func testThenPassthrough() { + let e = self.expectation(description: "Successful completion") + let queue = DispatchQueue.main + + let subject = PassthroughSubject() + let cancellable = subject.then { + ["A", "B", "C"].publisher.setFailureType(to: _CustomError.self) + }.sink(receiveCompletion: { _ in e.fulfill() }) { _ in return } + + queue.asyncAfter(deadline: .now() + .milliseconds(100)) { subject.send(0) } + queue.asyncAfter(deadline: .now() + .milliseconds(200)) { subject.send(1) } + queue.asyncAfter(deadline: .now() + .milliseconds(300)) { subject.send(2) } + queue.asyncAfter(deadline: .now() + .milliseconds(400)) { subject.send(completion: .finished) } + + self.wait(for: [e], timeout: 1) + cancellable.cancel() + } + + /// Tests the behavior of the `then` operator reacting to a upstream error. + func testThenFailure() { + let e = self.expectation(description: "Failure on origin") + let queue = DispatchQueue.main + + let subject = PassthroughSubject() + let cancellable = subject.then { + Future { (promise) in + queue.asyncAfter(deadline: .now() + .milliseconds(200)) { promise(.success("Completed")) } + } + }.sink(receiveCompletion: { (completion) in + guard case .failure = completion else { return } + e.fulfill() + }) { _ in return } + + queue.asyncAfter(deadline: .now() + .milliseconds(50)) { subject.send(0) } + queue.asyncAfter(deadline: .now() + .milliseconds(100)) { subject.send(completion: .failure(_CustomError())) } + + self.wait(for: [e], timeout: 2) + cancellable.cancel() + } } diff --git a/tests/runtime/publishers/DeferredCompleteTests.swift b/tests/runtime/publishers/DeferredCompleteTests.swift index bc8beae..b7c58f6 100644 --- a/tests/runtime/publishers/DeferredCompleteTests.swift +++ b/tests/runtime/publishers/DeferredCompleteTests.swift @@ -4,88 +4,88 @@ import Combine /// Tests the correct behavior of the `DeferredComplete` publisher. final class DeferredCompleteTests: XCTestCase { - override func setUp() { - self.continueAfterFailure = false - } - - /// A custom error to send as a dummy. - private struct CustomError: Swift.Error {} + override func setUp() { + self.continueAfterFailure = false + } + + /// A custom error to send as a dummy. + private struct _CustomError: Swift.Error {} } extension DeferredCompleteTests { - /// Tests a successful completion of the publisher. - func testSuccessfulEmptyCompletion() { - let exp = self.expectation(description: "Publisher completes successfully") - - let cancellable = DeferredComplete() - .handleEvents(receiveCancel: { XCTFail("The publisher has cancelled before completion") }) - .sink(receiveCompletion: { - guard case .finished = $0 else { return XCTFail("The successful completion publisher has failed!") } - exp.fulfill() - }, receiveValue: { _ in XCTFail("The empty complete publisher has emitted a value!") }) - - self.wait(for: [exp], timeout: 0.2) - cancellable.cancel() - } - - /// Tests a successful closure completion. - func testSuccessfulClosureCompletion() { - let exp = self.expectation(description: "Publisher completes successfully") - - let cancellable = DeferredComplete { return nil } - .handleEvents(receiveCancel: { XCTFail("The publisher has cancelled before completion") }) - .sink(receiveCompletion: { - guard case .finished = $0 else { return XCTFail("The successful completion publisher has failed!") } - exp.fulfill() - }, receiveValue: { _ in XCTFail("The empty complete publisher has emitted a value!") }) - - self.wait(for: [exp], timeout: 0.2) - cancellable.cancel() - } - - /// Tests a failure completion from an autoclosure. - func testFailedCompletion() { - let exp = self.expectation(description: "Publisher completes with a failure") - - let cancellable = DeferredComplete(error: .init()) - .handleEvents(receiveCancel: { XCTFail("The publisher has cancelled before completion") }) - .sink(receiveCompletion: { - guard case .failure = $0 else { return XCTFail("The failed completion publisher has completed successfully!") } - exp.fulfill() - }, receiveValue: { _ in XCTFail("The empty complete publisher has emitted a value!") }) - - self.wait(for: [exp], timeout: 0.2) - cancellable.cancel() - } - - /// Tests a failure completion from the full-fledge closure. - func testFailedClosureCompletion() { - let exp = self.expectation(description: "Publisher completes with a failure") - - let cancellable = DeferredComplete(output: Int.self) { return CustomError() } - .handleEvents(receiveCancel: { XCTFail("The publisher has cancelled before completion") }) - .sink(receiveCompletion: { - guard case .failure = $0 else { return XCTFail("The failed completion publisher has completed successfully!") } - exp.fulfill() - }, receiveValue: { _ in XCTFail("The empty complete publisher has emitted a value!") }) - - self.wait(for: [exp], timeout: 0.2) - cancellable.cancel() - } - - /// Tests the correct resource dumping. - func testPublisherPipeline() { - let exp = self.expectation(description: "Publisher completes successfully") - - let cancellable = DeferredComplete() - .map { $0 * 2 } - .handleEvents(receiveCancel: { XCTFail("The publisher has cancelled before completion") }) - .sink(receiveCompletion: { - guard case .finished = $0 else { return XCTFail("The successful completion publisher has failed!") } - exp.fulfill() - }, receiveValue: { _ in XCTFail("The empty complete publisher has emitted a value!") }) - - self.wait(for: [exp], timeout: 0.2) - cancellable.cancel() - } + /// Tests a successful completion of the publisher. + func testSuccessfulEmptyCompletion() { + let exp = self.expectation(description: "Publisher completes successfully") + + let cancellable = DeferredComplete() + .handleEvents(receiveCancel: { XCTFail("The publisher has cancelled before completion") }) + .sink(receiveCompletion: { + guard case .finished = $0 else { return XCTFail("The successful completion publisher has failed!") } + exp.fulfill() + }, receiveValue: { _ in XCTFail("The empty complete publisher has emitted a value!") }) + + self.wait(for: [exp], timeout: 0.2) + cancellable.cancel() + } + + /// Tests a successful closure completion. + func testSuccessfulClosureCompletion() { + let exp = self.expectation(description: "Publisher completes successfully") + + let cancellable = DeferredComplete { return nil } + .handleEvents(receiveCancel: { XCTFail("The publisher has cancelled before completion") }) + .sink(receiveCompletion: { + guard case .finished = $0 else { return XCTFail("The successful completion publisher has failed!") } + exp.fulfill() + }, receiveValue: { _ in XCTFail("The empty complete publisher has emitted a value!") }) + + self.wait(for: [exp], timeout: 0.2) + cancellable.cancel() + } + + /// Tests a failure completion from an autoclosure. + func testFailedCompletion() { + let exp = self.expectation(description: "Publisher completes with a failure") + + let cancellable = DeferredComplete(error: .init()) + .handleEvents(receiveCancel: { XCTFail("The publisher has cancelled before completion") }) + .sink(receiveCompletion: { + guard case .failure = $0 else { return XCTFail("The failed completion publisher has completed successfully!") } + exp.fulfill() + }, receiveValue: { _ in XCTFail("The empty complete publisher has emitted a value!") }) + + self.wait(for: [exp], timeout: 0.2) + cancellable.cancel() + } + + /// Tests a failure completion from the full-fledge closure. + func testFailedClosureCompletion() { + let exp = self.expectation(description: "Publisher completes with a failure") + + let cancellable = DeferredComplete(output: Int.self) { return _CustomError() } + .handleEvents(receiveCancel: { XCTFail("The publisher has cancelled before completion") }) + .sink(receiveCompletion: { + guard case .failure = $0 else { return XCTFail("The failed completion publisher has completed successfully!") } + exp.fulfill() + }, receiveValue: { _ in XCTFail("The empty complete publisher has emitted a value!") }) + + self.wait(for: [exp], timeout: 0.2) + cancellable.cancel() + } + + /// Tests the correct resource dumping. + func testPublisherPipeline() { + let exp = self.expectation(description: "Publisher completes successfully") + + let cancellable = DeferredComplete() + .map { $0 * 2 } + .handleEvents(receiveCancel: { XCTFail("The publisher has cancelled before completion") }) + .sink(receiveCompletion: { + guard case .finished = $0 else { return XCTFail("The successful completion publisher has failed!") } + exp.fulfill() + }, receiveValue: { _ in XCTFail("The empty complete publisher has emitted a value!") }) + + self.wait(for: [exp], timeout: 0.2) + cancellable.cancel() + } } diff --git a/tests/runtime/publishers/DeferredFutureTests.swift b/tests/runtime/publishers/DeferredFutureTests.swift index da8c8e1..e94bc3e 100644 --- a/tests/runtime/publishers/DeferredFutureTests.swift +++ b/tests/runtime/publishers/DeferredFutureTests.swift @@ -4,78 +4,78 @@ import Combine /// Tests the correct behavior of the `DeferredComplete` publisher. final class DeferredFutureTests: XCTestCase { - override func setUp() { - self.continueAfterFailure = false - } - - /// A custom error to send as a dummy. - private struct CustomError: Swift.Error {} + override func setUp() { + self.continueAfterFailure = false + } + + /// A custom error to send as a dummy. + private struct _CustomError: Swift.Error {} } extension DeferredFutureTests { - /// Tests a successful completion of the publisher. - func testSuccessfulSyncCompletion() { - let exp = self.expectation(description: "Publisher completes successfully") - - let value = 42 - let cancellable = DeferredFuture { (promise) in - promise(.success(value)) - }.handleEvents(receiveCancel: { XCTFail("The publisher has cancelled before completion") }) - .sink(receiveCompletion: { - guard case .finished = $0 else { return XCTFail("The successful completion publisher has failed!") } - exp.fulfill() - }, receiveValue: { XCTAssertEqual($0, value) }) - - self.wait(for: [exp], timeout: 0.2) - cancellable.cancel() - } - - /// Tests a successful completion of the publisher. - func testSuccessfulAsyncCompletion() { - let exp = self.expectation(description: "Publisher completes successfully") - - let value = 42 - let cancellable = DeferredFuture { (promise) in - DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(50)) { promise(.success(value)) } - }.handleEvents(receiveCancel: { XCTFail("The publisher has cancelled before completion") }) - .sink(receiveCompletion: { - guard case .finished = $0 else { return XCTFail("The successful completion publisher has failed!") } - exp.fulfill() - }, receiveValue: { XCTAssertEqual($0, value) }) - - self.wait(for: [exp], timeout: 0.2) - cancellable.cancel() - } - - /// Tests a successful completion of the publisher. - func testSuccessfulSyncFailure() { - let exp = self.expectation(description: "Publisher completes successfully") - - let cancellable = DeferredFuture { (promise) in - promise(.failure(CustomError())) - }.handleEvents(receiveCancel: { XCTFail("The publisher has cancelled before completion") }) - .sink(receiveCompletion: { - guard case .failure = $0 else { return XCTFail("The failure deferred future publisher has succeeded!") } - exp.fulfill() - }, receiveValue: { _ in XCTFail("No value was expected") }) - - self.wait(for: [exp], timeout: 0.2) - cancellable.cancel() - } - - /// Tests a successful completion of the publisher. - func testSuccessfulAsyncFailure() { - let exp = self.expectation(description: "Publisher completes successfully") - - let cancellable = DeferredFuture { (promise) in - DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(50)) { promise(.failure(CustomError())) } - }.handleEvents(receiveCancel: { XCTFail("The publisher has cancelled before completion") }) - .sink(receiveCompletion: { - guard case .failure = $0 else { return XCTFail("The failure deferred future publisher has failed!") } - exp.fulfill() - }, receiveValue: { _ in XCTFail("No value was expected") }) - - self.wait(for: [exp], timeout: 0.2) - cancellable.cancel() - } + /// Tests a successful completion of the publisher. + func testSuccessfulSyncCompletion() { + let exp = self.expectation(description: "Publisher completes successfully") + + let value = 42 + let cancellable = DeferredFuture { (promise) in + promise(.success(value)) + }.handleEvents(receiveCancel: { XCTFail("The publisher has cancelled before completion") }) + .sink(receiveCompletion: { + guard case .finished = $0 else { return XCTFail("The successful completion publisher has failed!") } + exp.fulfill() + }, receiveValue: { XCTAssertEqual($0, value) }) + + self.wait(for: [exp], timeout: 0.2) + cancellable.cancel() + } + + /// Tests a successful completion of the publisher. + func testSuccessfulAsyncCompletion() { + let exp = self.expectation(description: "Publisher completes successfully") + + let value = 42 + let cancellable = DeferredFuture { (promise) in + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(50)) { promise(.success(value)) } + }.handleEvents(receiveCancel: { XCTFail("The publisher has cancelled before completion") }) + .sink(receiveCompletion: { + guard case .finished = $0 else { return XCTFail("The successful completion publisher has failed!") } + exp.fulfill() + }, receiveValue: { XCTAssertEqual($0, value) }) + + self.wait(for: [exp], timeout: 0.2) + cancellable.cancel() + } + + /// Tests a successful completion of the publisher. + func testSuccessfulSyncFailure() { + let exp = self.expectation(description: "Publisher completes successfully") + + let cancellable = DeferredFuture { (promise) in + promise(.failure(_CustomError())) + }.handleEvents(receiveCancel: { XCTFail("The publisher has cancelled before completion") }) + .sink(receiveCompletion: { + guard case .failure = $0 else { return XCTFail("The failure deferred future publisher has succeeded!") } + exp.fulfill() + }, receiveValue: { _ in XCTFail("No value was expected") }) + + self.wait(for: [exp], timeout: 0.2) + cancellable.cancel() + } + + /// Tests a successful completion of the publisher. + func testSuccessfulAsyncFailure() { + let exp = self.expectation(description: "Publisher completes successfully") + + let cancellable = DeferredFuture { (promise) in + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(50)) { promise(.failure(_CustomError())) } + }.handleEvents(receiveCancel: { XCTFail("The publisher has cancelled before completion") }) + .sink(receiveCompletion: { + guard case .failure = $0 else { return XCTFail("The failure deferred future publisher has failed!") } + exp.fulfill() + }, receiveValue: { _ in XCTFail("No value was expected") }) + + self.wait(for: [exp], timeout: 0.2) + cancellable.cancel() + } } diff --git a/tests/runtime/publishers/DeferredPassthroughTests.swift b/tests/runtime/publishers/DeferredPassthroughTests.swift index a717e28..cbccd58 100644 --- a/tests/runtime/publishers/DeferredPassthroughTests.swift +++ b/tests/runtime/publishers/DeferredPassthroughTests.swift @@ -4,85 +4,85 @@ import Combine /// Tests the correct behavior of the `Complete` publisher. final class DeferredPassthroughTests: XCTestCase { - /// A convenience storage of cancellables. - private var _cancellables = Set() - - override func setUp() { - self.continueAfterFailure = false - self._cancellables.removeAll() - } - - override func tearDown() { - self._cancellables.removeAll() - } - - /// A custom error to send as a dummy. - private struct CustomError: Swift.Error {} + /// A convenience storage of cancellables. + private var _cancellables = Set() + + override func setUp() { + self.continueAfterFailure = false + self._cancellables.removeAll() + } + + override func tearDown() { + self._cancellables.removeAll() + } + + /// A custom error to send as a dummy. + private struct _CustomError: Swift.Error {} } extension DeferredPassthroughTests { - /// Tests a successful completion of the `DeferredCompletion` publisher. - func testSuccessfulCompletion() { - let exp = self.expectation(description: "Publisher completes successfully") - - let values: [Int] = [1, 2, 3, 4] - var receivedValues: [Int] = [] - - DeferredPassthrough { (subject) in - for i in values { subject.send(i) } - subject.send(completion: .finished) - }.sink(receiveCompletion: { - guard case .finished = $0 else { return XCTFail("The subject failed unexpectedly") } - exp.fulfill() - }, receiveValue: { receivedValues.append($0) }) - .store(in: &self._cancellables) - - self.wait(for: [exp], timeout: 0.5) - XCTAssertEqual(values, receivedValues) - } - - /// Tests a failure completion of the `DeferredCompletion` publisher. - func testFailedCompletion() { - let exp = self.expectation(description: "Publisher completes successfully") - - let values: [Int] = [1, 2, 3, 4] - var receivedValues: [Int] = [] - - DeferredPassthrough { (subject) in - for i in values { subject.send(i) } - subject.send(completion: .failure(.init())) - }.sink(receiveCompletion: { - guard case .failure = $0 else { return XCTFail("The subject succeeeded unexpectedly") } - exp.fulfill() - }, receiveValue: { receivedValues.append($0) }) - .store(in: &self._cancellables) - - self.wait(for: [exp], timeout: 0.5) - XCTAssertEqual(values, receivedValues) - } - - func testBackpressure() { - let exp = self.expectation(description: "Publisher completes successfully") - - let values: [Int] = [1, 2, 3, 4] - var receivedValues: [Int] = [] - - let oneByOneSubscriber = AnySubscriber(receiveSubscription: { - $0.request(.max(2)) - }, receiveValue: { - receivedValues.append($0) - return .none - }, receiveCompletion: { - guard case .finished = $0 else { return XCTFail("The subject failed unexpectedly") } - exp.fulfill() - }) - - DeferredPassthrough { (subject) in - for i in values { subject.send(i) } - subject.send(completion: .finished) - }.subscribe(oneByOneSubscriber) - - self.wait(for: [exp], timeout: 100) - XCTAssertEqual(.init(values.prefix(upTo: 2)), receivedValues) - } + /// Tests a successful completion of the `DeferredCompletion` publisher. + func testSuccessfulCompletion() { + let exp = self.expectation(description: "Publisher completes successfully") + + let values: [Int] = [1, 2, 3, 4] + var receivedValues: [Int] = [] + + DeferredPassthrough { (subject) in + for i in values { subject.send(i) } + subject.send(completion: .finished) + }.sink(receiveCompletion: { + guard case .finished = $0 else { return XCTFail("The subject failed unexpectedly") } + exp.fulfill() + }, receiveValue: { receivedValues.append($0) }) + .store(in: &self._cancellables) + + self.wait(for: [exp], timeout: 0.5) + XCTAssertEqual(values, receivedValues) + } + + /// Tests a failure completion of the `DeferredCompletion` publisher. + func testFailedCompletion() { + let exp = self.expectation(description: "Publisher completes successfully") + + let values: [Int] = [1, 2, 3, 4] + var receivedValues: [Int] = [] + + DeferredPassthrough { (subject) in + for i in values { subject.send(i) } + subject.send(completion: .failure(.init())) + }.sink(receiveCompletion: { + guard case .failure = $0 else { return XCTFail("The subject succeeeded unexpectedly") } + exp.fulfill() + }, receiveValue: { receivedValues.append($0) }) + .store(in: &self._cancellables) + + self.wait(for: [exp], timeout: 0.5) + XCTAssertEqual(values, receivedValues) + } + + func testBackpressure() { + let exp = self.expectation(description: "Publisher completes successfully") + + let values: [Int] = [1, 2, 3, 4] + var receivedValues: [Int] = [] + + let oneByOneSubscriber = AnySubscriber(receiveSubscription: { + $0.request(.max(2)) + }, receiveValue: { + receivedValues.append($0) + return .none + }, receiveCompletion: { + guard case .finished = $0 else { return XCTFail("The subject failed unexpectedly") } + exp.fulfill() + }) + + DeferredPassthrough { (subject) in + for i in values { subject.send(i) } + subject.send(completion: .finished) + }.subscribe(oneByOneSubscriber) + + self.wait(for: [exp], timeout: 100) + XCTAssertEqual(.init(values.prefix(upTo: 2)), receivedValues) + } } diff --git a/tests/runtime/publishers/DeferredResultTests.swift b/tests/runtime/publishers/DeferredResultTests.swift index eff95f9..2def53b 100644 --- a/tests/runtime/publishers/DeferredResultTests.swift +++ b/tests/runtime/publishers/DeferredResultTests.swift @@ -4,51 +4,51 @@ import Combine /// Tests the correct behavior of the `Complete` publisher. final class DeferredResultTests: XCTestCase { - /// A convenience storage of cancellables. - private var _cancellables = Set() - - override func setUp() { - self.continueAfterFailure = false - self._cancellables.removeAll() - } - - override func tearDown() { - self._cancellables.removeAll() - } - - /// A custom error to send as a dummy. - private struct CustomError: Swift.Error {} + /// A convenience storage of cancellables. + private var _cancellables = Set() + + override func setUp() { + self.continueAfterFailure = false + self._cancellables.removeAll() + } + + override func tearDown() { + self._cancellables.removeAll() + } + + /// A custom error to send as a dummy. + private struct _CustomError: Swift.Error {} } extension DeferredResultTests { - /// Tests a successful completion of the `DeferredCompletion` publisher. - func testSuccessfulCompletion() { - let exp = self.expectation(description: "Publisher completes successfully") - let value = 1 - - DeferredResult { .success(value) } - .handleEvents(receiveCancel: { XCTFail("The publisher has cancelled before completion") }) - .sink(receiveCompletion: { - guard case .finished = $0 else { return XCTFail("The successful completion publisher has failed!") } - exp.fulfill() - }, receiveValue: { XCTAssertEqual($0, value) }) - .store(in: &self._cancellables) - - self.wait(for: [exp], timeout: 0.2) - } - - /// Tests a failure completion of the `DeferredCompletion` publisher. - func testFailedCompletion() { - let exp = self.expectation(description: "Publisher completes with a failure") - - DeferredResult { .failure(CustomError()) } - .handleEvents(receiveCancel: { XCTFail("The publisher has cancelled before completion") }) - .sink(receiveCompletion: { - guard case .failure = $0 else { return XCTFail("The failed completion publisher has completed successfully!") } - exp.fulfill() - }, receiveValue: { _ in XCTFail("The empty complete publisher has emitted a value!") }) - .store(in: &self._cancellables) - - self.wait(for: [exp], timeout: 0.2) - } + /// Tests a successful completion of the `DeferredCompletion` publisher. + func testSuccessfulCompletion() { + let exp = self.expectation(description: "Publisher completes successfully") + let value = 1 + + DeferredResult { .success(value) } + .handleEvents(receiveCancel: { XCTFail("The publisher has cancelled before completion") }) + .sink(receiveCompletion: { + guard case .finished = $0 else { return XCTFail("The successful completion publisher has failed!") } + exp.fulfill() + }, receiveValue: { XCTAssertEqual($0, value) }) + .store(in: &self._cancellables) + + self.wait(for: [exp], timeout: 0.2) + } + + /// Tests a failure completion of the `DeferredCompletion` publisher. + func testFailedCompletion() { + let exp = self.expectation(description: "Publisher completes with a failure") + + DeferredResult { .failure(_CustomError()) } + .handleEvents(receiveCancel: { XCTFail("The publisher has cancelled before completion") }) + .sink(receiveCompletion: { + guard case .failure = $0 else { return XCTFail("The failed completion publisher has completed successfully!") } + exp.fulfill() + }, receiveValue: { _ in XCTFail("The empty complete publisher has emitted a value!") }) + .store(in: &self._cancellables) + + self.wait(for: [exp], timeout: 0.2) + } } diff --git a/tests/runtime/publishers/DeferredTryCompleteTests.swift b/tests/runtime/publishers/DeferredTryCompleteTests.swift index 84026f1..400548a 100644 --- a/tests/runtime/publishers/DeferredTryCompleteTests.swift +++ b/tests/runtime/publishers/DeferredTryCompleteTests.swift @@ -4,81 +4,81 @@ import Combine /// Tests the correct behavior of the `DeferredTryComplete` publisher. final class DeferredTryCompleteTests: XCTestCase { - /// A convenience storage of cancellables. - private var _cancellables = Set() - - override func setUp() { - self.continueAfterFailure = false - self._cancellables.removeAll() - } - - override func tearDown() { - self._cancellables.removeAll() - } - - /// A custom error to send as a dummy. - private struct CustomError: Swift.Error {} + /// A convenience storage of cancellables. + private var _cancellables = Set() + + override func setUp() { + self.continueAfterFailure = false + self._cancellables.removeAll() + } + + override func tearDown() { + self._cancellables.removeAll() + } + + /// A custom error to send as a dummy. + private struct _CustomError: Swift.Error {} } extension DeferredTryCompleteTests { - /// Tests a successful completion of the publisher. - func testSuccessfulEmptyCompletion() { - let exp = self.expectation(description: "Publisher completes successfully") - - DeferredTryComplete() - .handleEvents(receiveCancel: { XCTFail("The publisher has cancelled before completion") }) - .sink(receiveCompletion: { - guard case .finished = $0 else { return XCTFail("The successful completion publisher has failed!") } - exp.fulfill() - }, receiveValue: { _ in XCTFail("The empty complete publisher has emitted a value!") }) - .store(in: &self._cancellables) - - self.wait(for: [exp], timeout: 0.2) - } - - /// Tests a successful closure completion. - func testSuccessfulClosureCompletion() { - let exp = self.expectation(description: "Publisher completes successfully") - - DeferredTryComplete { return } - .handleEvents(receiveCancel: { XCTFail("The publisher has cancelled before completion") }) - .sink(receiveCompletion: { - guard case .finished = $0 else { return XCTFail("The successful completion publisher has failed!") } - exp.fulfill() - }, receiveValue: { _ in XCTFail("The empty complete publisher has emitted a value!") }) - .store(in: &self._cancellables) - - self.wait(for: [exp], timeout: 0.2) - } - - /// Tests a failure completion from the closure - func testFailedCompletion() { - let exp = self.expectation(description: "Publisher completes with a failure") - - DeferredTryComplete { throw CustomError() } - .handleEvents(receiveCancel: { XCTFail("The publisher has cancelled before completion") }) - .sink(receiveCompletion: { - guard case .failure = $0 else { return XCTFail("The failed completion publisher has completed successfully!") } - exp.fulfill() - }, receiveValue: { _ in XCTFail("The empty complete publisher has emitted a value!") }) - .store(in: &self._cancellables) - - self.wait(for: [exp], timeout: 0.2) - } - - /// Tests the correct resource dumping. - func testPublisherPipeline() { - let exp = self.expectation(description: "Publisher completes successfully") - - DeferredTryComplete() - .map { $0 * 2 } - .handleEvents(receiveCancel: { XCTFail("The publisher has cancelled before completion") }) - .sink(receiveCompletion: { - guard case .finished = $0 else { return XCTFail("The successful completion publisher has failed!") } - exp.fulfill() - }, receiveValue: { _ in XCTFail("The empty complete publisher has emitted a value!") }) - .store(in: &self._cancellables) - - self.wait(for: [exp], timeout: 0.2) - } + /// Tests a successful completion of the publisher. + func testSuccessfulEmptyCompletion() { + let exp = self.expectation(description: "Publisher completes successfully") + + DeferredTryComplete() + .handleEvents(receiveCancel: { XCTFail("The publisher has cancelled before completion") }) + .sink(receiveCompletion: { + guard case .finished = $0 else { return XCTFail("The successful completion publisher has failed!") } + exp.fulfill() + }, receiveValue: { _ in XCTFail("The empty complete publisher has emitted a value!") }) + .store(in: &self._cancellables) + + self.wait(for: [exp], timeout: 0.2) + } + + /// Tests a successful closure completion. + func testSuccessfulClosureCompletion() { + let exp = self.expectation(description: "Publisher completes successfully") + + DeferredTryComplete { return } + .handleEvents(receiveCancel: { XCTFail("The publisher has cancelled before completion") }) + .sink(receiveCompletion: { + guard case .finished = $0 else { return XCTFail("The successful completion publisher has failed!") } + exp.fulfill() + }, receiveValue: { _ in XCTFail("The empty complete publisher has emitted a value!") }) + .store(in: &self._cancellables) + + self.wait(for: [exp], timeout: 0.2) + } + + /// Tests a failure completion from the closure + func testFailedCompletion() { + let exp = self.expectation(description: "Publisher completes with a failure") + + DeferredTryComplete { throw _CustomError() } + .handleEvents(receiveCancel: { XCTFail("The publisher has cancelled before completion") }) + .sink(receiveCompletion: { + guard case .failure = $0 else { return XCTFail("The failed completion publisher has completed successfully!") } + exp.fulfill() + }, receiveValue: { _ in XCTFail("The empty complete publisher has emitted a value!") }) + .store(in: &self._cancellables) + + self.wait(for: [exp], timeout: 0.2) + } + + /// Tests the correct resource dumping. + func testPublisherPipeline() { + let exp = self.expectation(description: "Publisher completes successfully") + + DeferredTryComplete() + .map { $0 * 2 } + .handleEvents(receiveCancel: { XCTFail("The publisher has cancelled before completion") }) + .sink(receiveCompletion: { + guard case .finished = $0 else { return XCTFail("The successful completion publisher has failed!") } + exp.fulfill() + }, receiveValue: { _ in XCTFail("The empty complete publisher has emitted a value!") }) + .store(in: &self._cancellables) + + self.wait(for: [exp], timeout: 0.2) + } } diff --git a/tests/runtime/publishers/DeferredTryValueTests.swift b/tests/runtime/publishers/DeferredTryValueTests.swift index 3216c24..4cb2d3a 100644 --- a/tests/runtime/publishers/DeferredTryValueTests.swift +++ b/tests/runtime/publishers/DeferredTryValueTests.swift @@ -4,49 +4,49 @@ import Combine /// Tests the correct behavior of the `DeferredTryValue` publisher. final class DeferredTryValueTests: XCTestCase { - /// A convenience storage of cancellables. - private var _cancellables = Set() - - override func setUp() { - self.continueAfterFailure = false - self._cancellables.removeAll() - } - - override func tearDown() { - self._cancellables.removeAll() - } - - /// A custom error to send as a dummy. - private struct CustomError: Swift.Error {} + /// A convenience storage of cancellables. + private var _cancellables = Set() + + override func setUp() { + self.continueAfterFailure = false + self._cancellables.removeAll() + } + + override func tearDown() { + self._cancellables.removeAll() + } + + /// A custom error to send as a dummy. + private struct _CustomError: Swift.Error {} } extension DeferredTryValueTests { - /// Tests a successful delivery of an emitted value. - func testSuccessfulDelivery() { - let exp = self.expectation(description: "Publisher completes successfully") - - let value = 42 - DeferredTryValue { value } - .sink(receiveCompletion: { - guard case .finished = $0 else { return XCTFail("The deffered value publisher has failed!") } - exp.fulfill() - }, receiveValue: { XCTAssertEqual($0, value) }) - .store(in: &self._cancellables) - - self.wait(for: [exp], timeout: 0.2) - } - - /// Tests a failed value generation. - func testFailedCompletion() { - let exp = self.expectation(description: "Publisher completes with a failure") - - DeferredTryValue { throw CustomError() } - .sink(receiveCompletion: { - guard case .failure = $0 else { return XCTFail("The deffered value publisher has completed successfully!") } - exp.fulfill() - }, receiveValue: { _ in XCTFail("The deferred complete has emitted a value!") }) - .store(in: &self._cancellables) - - self.wait(for: [exp], timeout: 0.2) - } + /// Tests a successful delivery of an emitted value. + func testSuccessfulDelivery() { + let exp = self.expectation(description: "Publisher completes successfully") + + let value = 42 + DeferredTryValue { value } + .sink(receiveCompletion: { + guard case .finished = $0 else { return XCTFail("The deffered value publisher has failed!") } + exp.fulfill() + }, receiveValue: { XCTAssertEqual($0, value) }) + .store(in: &self._cancellables) + + self.wait(for: [exp], timeout: 0.2) + } + + /// Tests a failed value generation. + func testFailedCompletion() { + let exp = self.expectation(description: "Publisher completes with a failure") + + DeferredTryValue { throw _CustomError() } + .sink(receiveCompletion: { + guard case .failure = $0 else { return XCTFail("The deffered value publisher has completed successfully!") } + exp.fulfill() + }, receiveValue: { _ in XCTFail("The deferred complete has emitted a value!") }) + .store(in: &self._cancellables) + + self.wait(for: [exp], timeout: 0.2) + } } diff --git a/tests/runtime/publishers/DeferredValueTests.swift b/tests/runtime/publishers/DeferredValueTests.swift index 19778bc..8be9c6e 100644 --- a/tests/runtime/publishers/DeferredValueTests.swift +++ b/tests/runtime/publishers/DeferredValueTests.swift @@ -4,35 +4,32 @@ import Combine /// Tests the correct behavior of the `DeferredValueTests` publisher. final class DeferredValueTests: XCTestCase { - /// A convenience storage of cancellables. - private var _cancellables = Set() - - override func setUp() { - self.continueAfterFailure = false - self._cancellables.removeAll() - } - - override func tearDown() { - self._cancellables.removeAll() - } - - /// A custom error to send as a dummy. - private struct CustomError: Swift.Error {} + /// A convenience storage of cancellables. + private var _cancellables = Set() + + override func setUp() { + self.continueAfterFailure = false + self._cancellables.removeAll() + } + + override func tearDown() { + self._cancellables.removeAll() + } } extension DeferredValueTests { - /// Tests a successful delivery of an emitted value. - func testSuccessfulDelivery() { - let exp = self.expectation(description: "Publisher completes successfully") - - let value = 42 - DeferredTryValue { value } - .sink(receiveCompletion: { - guard case .finished = $0 else { return XCTFail("The deffered value publisher has failed!") } - exp.fulfill() - }, receiveValue: { XCTAssertEqual($0, value) }) - .store(in: &self._cancellables) + /// Tests a successful delivery of an emitted value. + func testSuccessfulDelivery() { + let exp = self.expectation(description: "Publisher completes successfully") + + let value = 42 + DeferredTryValue { value } + .sink(receiveCompletion: { + guard case .finished = $0 else { return XCTFail("The deffered value publisher has failed!") } + exp.fulfill() + }, receiveValue: { XCTAssertEqual($0, value) }) + .store(in: &self._cancellables) - self.wait(for: [exp], timeout: 0.2) - } + self.wait(for: [exp], timeout: 0.2) + } } diff --git a/tests/runtime/subscribers/FixedSinkTests.swift b/tests/runtime/subscribers/FixedSinkTests.swift index f7affbf..d5970d7 100644 --- a/tests/runtime/subscribers/FixedSinkTests.swift +++ b/tests/runtime/subscribers/FixedSinkTests.swift @@ -4,84 +4,83 @@ import Combine /// Tests the correct behavior of the `Then` operator. final class FixedSinkTests: XCTestCase { - /// A custom error to send as a dummy. - private struct CustomError: Swift.Error {} - - override func setUp() { - self.continueAfterFailure = false - } + override func setUp() { + self.continueAfterFailure = false + } + /// A custom error to send as a dummy. + private struct _CustomError: Swift.Error {} } extension FixedSinkTests { - /// Tests a subscription with a fixed sink yielding some values and a successful completion. - func testSuccessfulCompletion() { - let e = self.expectation(description: "Successful completion") - - let input = (0..<10) - var received = [Int]() - - let subscriber = Subscribers.FixedSink(demand: input.count, receiveCompletion: { - guard case .finished = $0 else { return XCTFail("A failure completion was received when a successful completion was expected.")} - e.fulfill() - }, receiveValue: { received.append($0) }) - - input.publisher.setFailureType(to: CustomError.self) - .map { $0 * 2} - .subscribe(subscriber) - - self.wait(for: [e], timeout: 1) - XCTAssertEqual(input.map { $0 * 2 }, received) - subscriber.cancel() - } - - /// Tests a subscription with a fixed sink yielding some values and a successful completion. - func testCutCompletion() { - let e = self.expectation(description: "Successful completion") - - let input = (0..<10) - var received = [Int]() - - let subscriber = Subscribers.FixedSink(demand: 3, receiveCompletion: { - guard case .finished = $0 else { return XCTFail("A failure completion was received when a successful completion was expected.")} - e.fulfill() - }, receiveValue: { received.append($0) }) - - input.publisher.setFailureType(to: CustomError.self) - .map { $0 * 2} - .subscribe(subscriber) - - self.wait(for: [e], timeout: 1) - XCTAssertEqual(input.prefix(upTo: 3).map { $0 * 2 }, received) - subscriber.cancel() + /// Tests a subscription with a fixed sink yielding some values and a successful completion. + func testSuccessfulCompletion() { + let e = self.expectation(description: "Successful completion") + + let input = (0..<10) + var received = [Int]() + + let subscriber = Subscribers.FixedSink(demand: input.count, receiveCompletion: { + guard case .finished = $0 else { return XCTFail("A failure completion was received when a successful completion was expected.")} + e.fulfill() + }, receiveValue: { received.append($0) }) + + input.publisher.setFailureType(to: _CustomError.self) + .map { $0 * 2} + .subscribe(subscriber) + + self.wait(for: [e], timeout: 1) + XCTAssertEqual(input.map { $0 * 2 }, received) + subscriber.cancel() + } + + /// Tests a subscription with a fixed sink yielding some values and a successful completion. + func testCutCompletion() { + let e = self.expectation(description: "Successful completion") + + let input = (0..<10) + var received = [Int]() + + let subscriber = Subscribers.FixedSink(demand: 3, receiveCompletion: { + guard case .finished = $0 else { return XCTFail("A failure completion was received when a successful completion was expected.")} + e.fulfill() + }, receiveValue: { received.append($0) }) + + input.publisher.setFailureType(to: _CustomError.self) + .map { $0 * 2} + .subscribe(subscriber) + + self.wait(for: [e], timeout: 1) + XCTAssertEqual(input.prefix(upTo: 3).map { $0 * 2 }, received) + subscriber.cancel() + } + + /// Tests a subscription with a fixed sink yielding some values and a failure completion. + func testFailedCompletion() { + let e = self.expectation(description: "Failure completion") + + let input = (0..<5) + var received = [Int]() + + let subscriber = Subscribers.FixedSink(demand: input.count + 1, receiveCompletion: { + guard case .failure = $0 else { return XCTFail("A succesful completion was received when a failure completion was expected.")} + e.fulfill() + }, receiveValue: { received.append($0) }) + + let subject = PassthroughSubject() + subject.map { $0 * 2} + .subscribe(subscriber) + + let queue = DispatchQueue(label: "io.dehesa.conbini.tests.subscribers.fixedSink") + for i in input { + queue.asyncAfter(deadline: .now() + .milliseconds(i * 10)) { subject.send(i) } } - - /// Tests a subscription with a fixed sink yielding some values and a failure completion. - func testFailedCompletion() { - let e = self.expectation(description: "Failure completion") - - let input = (0..<5) - var received = [Int]() - - let subscriber = Subscribers.FixedSink(demand: input.count + 1, receiveCompletion: { - guard case .failure = $0 else { return XCTFail("A succesful completion was received when a failure completion was expected.")} - e.fulfill() - }, receiveValue: { received.append($0) }) - - let subject = PassthroughSubject() - subject.map { $0 * 2} - .subscribe(subscriber) - - let queue = DispatchQueue(label: "io.dehesa.conbini.tests.subscribers.fixedSink") - for i in input { - queue.asyncAfter(deadline: .now() + .milliseconds(i * 10)) { subject.send(i) } - } - - queue.asyncAfter(deadline: .now() + .milliseconds((input.last! + 1) * 10)) { - subject.send(completion: .failure(CustomError())) - } - - self.wait(for: [e], timeout: 1) - XCTAssertEqual(input.map { $0 * 2 }, received) - subscriber.cancel() + + queue.asyncAfter(deadline: .now() + .milliseconds((input.last! + 1) * 10)) { + subject.send(completion: .failure(_CustomError())) } + + self.wait(for: [e], timeout: 1) + XCTAssertEqual(input.map { $0 * 2 }, received) + subscriber.cancel() + } } diff --git a/tests/runtime/subscribers/GraduatedSinkTests.swift b/tests/runtime/subscribers/GraduatedSinkTests.swift index 5178fde..62372fe 100644 --- a/tests/runtime/subscribers/GraduatedSinkTests.swift +++ b/tests/runtime/subscribers/GraduatedSinkTests.swift @@ -4,63 +4,62 @@ import Combine /// Tests the correct behavior of the `Then` operator. final class GraduatedSinkTests: XCTestCase { - /// A custom error to send as a dummy. - private struct CustomError: Swift.Error {} - - override func setUp() { - self.continueAfterFailure = false - } + override func setUp() { + self.continueAfterFailure = false + } + /// A custom error to send as a dummy. + private struct _CustomError: Swift.Error {} } extension GraduatedSinkTests { - /// Tests a subscription with a graduated sink yielding some values and a successful completion. - func testSuccessfulCompletion() { - let e = self.expectation(description: "Successful completion") - - let input = (0..<10) - var received = [Int]() - - let subscriber = Subscribers.GraduatedSink(maxDemand: .max(3), receiveCompletion: { - guard case .finished = $0 else { return XCTFail("A failure completion was received when a successful completion was expected.")} - e.fulfill() - }, receiveValue: { received.append($0) }) - - input.publisher.setFailureType(to: CustomError.self) - .map { $0 * 2} - .subscribe(subscriber) - - self.wait(for: [e], timeout: 1) - XCTAssertEqual(input.map { $0 * 2 }, received) - subscriber.cancel() + /// Tests a subscription with a graduated sink yielding some values and a successful completion. + func testSuccessfulCompletion() { + let e = self.expectation(description: "Successful completion") + + let input = (0..<10) + var received = [Int]() + + let subscriber = Subscribers.GraduatedSink(maxDemand: .max(3), receiveCompletion: { + guard case .finished = $0 else { return XCTFail("A failure completion was received when a successful completion was expected.")} + e.fulfill() + }, receiveValue: { received.append($0) }) + + input.publisher.setFailureType(to: _CustomError.self) + .map { $0 * 2} + .subscribe(subscriber) + + self.wait(for: [e], timeout: 1) + XCTAssertEqual(input.map { $0 * 2 }, received) + subscriber.cancel() + } + + /// Tests a subscription with a graduated sink yielding some values and a failure completion. + func testFailedCompletion() { + let e = self.expectation(description: "Failure completion") + + let input = (0..<10) + var received = [Int]() + + let subscriber = Subscribers.GraduatedSink(maxDemand: .max(3), receiveCompletion: { + guard case .failure = $0 else { return XCTFail("A succesful completion was received when a failure completion was expected.")} + e.fulfill() + }, receiveValue: { received.append($0) }) + + let subject = PassthroughSubject() + subject.map { $0 * 2} + .subscribe(subscriber) + + let queue = DispatchQueue(label: "io.dehesa.conbini.tests.subscribers.graduatedSink") + for i in input { + queue.asyncAfter(deadline: .now() + .milliseconds(i * 50)) { subject.send(i) } } - /// Tests a subscription with a graduated sink yielding some values and a failure completion. - func testFailedCompletion() { - let e = self.expectation(description: "Failure completion") - - let input = (0..<10) - var received = [Int]() - - let subscriber = Subscribers.GraduatedSink(maxDemand: .max(3), receiveCompletion: { - guard case .failure = $0 else { return XCTFail("A succesful completion was received when a failure completion was expected.")} - e.fulfill() - }, receiveValue: { received.append($0) }) - - let subject = PassthroughSubject() - subject.map { $0 * 2} - .subscribe(subscriber) - - let queue = DispatchQueue(label: "io.dehesa.conbini.tests.subscribers.graduatedSink") - for i in input { - queue.asyncAfter(deadline: .now() + .milliseconds(i * 50)) { subject.send(i) } - } - - queue.asyncAfter(deadline: .now() + .milliseconds((input.last! + 1) * 50)) { - subject.send(completion: .failure(CustomError())) - } - - self.wait(for: [e], timeout: 4) - XCTAssertEqual(input.map { $0 * 2 }, received) - subscriber.cancel() + queue.asyncAfter(deadline: .now() + .milliseconds((input.last! + 1) * 50)) { + subject.send(completion: .failure(_CustomError())) } + + self.wait(for: [e], timeout: 4) + XCTAssertEqual(input.map { $0 * 2 }, received) + subscriber.cancel() + } } diff --git a/tests/runtime/utils/BufferTests.swift b/tests/runtime/utils/BufferTests.swift index 18b6bbd..12b9bd1 100644 --- a/tests/runtime/utils/BufferTests.swift +++ b/tests/runtime/utils/BufferTests.swift @@ -4,68 +4,72 @@ import Combine /// Tests the correct behavior of the `DeferredComplete` publisher. final class BufferTests: XCTestCase { - /// A convenience storage of cancellables. - private var cancellables = Set() - - override func setUp() { - self.continueAfterFailure = false - self.cancellables.removeAll() - } - - /// A custom error to send as a dummy. - private struct CustomError: Swift.Error {} + /// A convenience storage of cancellables. + private var cancellables = Set() + + override func setUp() { + self.continueAfterFailure = false + self.cancellables.removeAll() + } + + override func tearDown() { + self._cancellables.removeAll() + } + + /// A custom error to send as a dummy. + private struct _CustomError: Swift.Error {} } extension BufferTests { - /// Tests the regular usage of a buffer operator. - func testRegularUsage() { - let exp = self.expectation(description: "Publisher completes successfully") - - let input = [0, 1, 2, 3, 4] - var output = [Int]() - - let subject = PassthroughSubject() - let subscriber = AnySubscriber(receiveSubscription: { $0.request(.max(1)) }, receiveValue: { - output.append($0) - return .max(1) - }, receiveCompletion: { - guard case .finished = $0 else { return XCTFail("The publisher failed when a successful completion was expected")} - exp.fulfill() - }) - - subject.map { $0 * 2 } - .buffer(size: 10, prefetch: .keepFull, whenFull: .fatalError()) - .map { $0 * 2} - .subscribe(subscriber) - - for i in input { subject.send(i) } - subject.send(completion: .finished) - - self.wait(for: [exp], timeout: 0.2) - XCTAssertEqual(input.map { $0 * 4 }, output) - } - - /// Tests a buffer operator when it is filled till the *brim*. - func testBrimUsage() { - let exp = self.expectation(description: "Publisher completes successfully") - - let input = [0, 1, 2, 3, 4] - var output = [Int]() - - let subject = PassthroughSubject() - subject.map { $0 * 2 } - .buffer(size: input.count, prefetch: .keepFull, whenFull: .fatalError()) - .map { $0 * 2} - .sink(receiveCompletion: { - guard case .finished = $0 else { return XCTFail("The publisher failed when a successful completion was expected")} - exp.fulfill() - }, receiveValue: { output.append($0) }) - .store(in: &self.cancellables) - - for i in input { subject.send(i) } - subject.send(completion: .finished) - - self.wait(for: [exp], timeout: 0.2) - XCTAssertEqual(input.map { $0 * 4 }, output) - } + /// Tests the regular usage of a buffer operator. + func testRegularUsage() { + let exp = self.expectation(description: "Publisher completes successfully") + + let input = [0, 1, 2, 3, 4] + var output = [Int]() + + let subject = PassthroughSubject() + let subscriber = AnySubscriber(receiveSubscription: { $0.request(.max(1)) }, receiveValue: { + output.append($0) + return .max(1) + }, receiveCompletion: { + guard case .finished = $0 else { return XCTFail("The publisher failed when a successful completion was expected")} + exp.fulfill() + }) + + subject.map { $0 * 2 } + .buffer(size: 10, prefetch: .keepFull, whenFull: .fatalError()) + .map { $0 * 2} + .subscribe(subscriber) + + for i in input { subject.send(i) } + subject.send(completion: .finished) + + self.wait(for: [exp], timeout: 0.2) + XCTAssertEqual(input.map { $0 * 4 }, output) + } + + /// Tests a buffer operator when it is filled till the *brim*. + func testBrimUsage() { + let exp = self.expectation(description: "Publisher completes successfully") + + let input = [0, 1, 2, 3, 4] + var output = [Int]() + + let subject = PassthroughSubject() + subject.map { $0 * 2 } + .buffer(size: input.count, prefetch: .keepFull, whenFull: .fatalError()) + .map { $0 * 2} + .sink(receiveCompletion: { + guard case .finished = $0 else { return XCTFail("The publisher failed when a successful completion was expected")} + exp.fulfill() + }, receiveValue: { output.append($0) }) + .store(in: &self.cancellables) + + for i in input { subject.send(i) } + subject.send(completion: .finished) + + self.wait(for: [exp], timeout: 0.2) + XCTAssertEqual(input.map { $0 * 4 }, output) + } } diff --git a/tests/testing/ExpectationsTests.swift b/tests/testing/ExpectationsTests.swift index 84f5046..4418a03 100644 --- a/tests/testing/ExpectationsTests.swift +++ b/tests/testing/ExpectationsTests.swift @@ -5,80 +5,80 @@ import Combine /// Tests the correct behavior of the *expectation* conveniences. final class ExpectationsTests: XCTestCase { - /// A custom error to send as a dummy. - private struct CustomError: Swift.Error {} - - override func setUp() { - self.continueAfterFailure = false - } + override func setUp() { + self.continueAfterFailure = false + } + + /// A custom error to send as a dummy. + private struct _CustomError: Swift.Error {} } extension ExpectationsTests { - /// Tests successful completion expectations. - func testCompletionExpectations() { - [0, 2, 4, 6, 8].publisher.expectsCompletion(timeout: 0.2, on: self) - - DeferredPassthrough { (subject) in - subject.send(0) - DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(20)) { subject.send(2) } - DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(40)) { subject.send(4) } - DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(60)) { subject.send(6) } - DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(80)) { subject.send(completion: .finished) } - }.expectsCompletion(timeout: 0.2, on: self) - } - - /// Tests failure completion expectations. - func testFailedExpectations() { - Fail(error: .init()).expectsFailure(timeout: 0.2, on: self) - - DeferredPassthrough { (subject) in - subject.send(0) - DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(20)) { subject.send(2) } - DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(40)) { subject.send(4) } - DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(60)) { subject.send(6) } - DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(80)) { subject.send(completion: .failure(CustomError())) } - }.expectsFailure(timeout: 0.2, on: self) - } - - /// Tests one single value emission expectations. - func testSingleValueEmissionExpectations() { - Just(0).expectsOne(timeout: 0.2, on: self) - - DeferredPassthrough { (subject) in - DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(20)) { subject.send(0) } - DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(80)) { subject.send(completion: .failure(CustomError())) } - }.expectsFailure(timeout: 0.2, on: self) - } - - /// Tests the reception of all emitted values. - func testAllEmissionExpectations() { - let values = [0, 2, 4, 6, 8] - - let sequenceEmitted = values.publisher.expectsAll(timeout: 0.2, on: self) - XCTAssertEqual(values, sequenceEmitted) - - let subjectEmitted = DeferredPassthrough { (subject) in - for (index, value) in values.enumerated() { - DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(index*10)) { subject.send(value) } - } - DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(values.count*10)) { subject.send(completion: .finished) } - }.expectsAll(timeout: 0.2, on: self) - XCTAssertEqual(values, subjectEmitted) - } - - /// Tests the reception of at least a given amount of values. - func testAtLeastEmisionExpectations() { - let values = [0, 2, 4, 6, 8] - - let sequenceEmitted = values.publisher.expectsAtLeast(values: 2, timeout: 0.2, on: self) - XCTAssertEqual(.init(values[0..<2]), sequenceEmitted) - - let subjectEmitted = DeferredPassthrough { (subject) in - for (index, value) in values.enumerated() { - DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(index*10)) { subject.send(value) } - } - DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(values.count*10)) { subject.send(completion: .finished) } - }.expectsAtLeast(values: 2, timeout: 0.2, on: self) - XCTAssertEqual(.init(values[0..<2]), subjectEmitted) - } + /// Tests successful completion expectations. + func testCompletionExpectations() { + [0, 2, 4, 6, 8].publisher.expectsCompletion(timeout: 0.2, on: self) + + DeferredPassthrough { (subject) in + subject.send(0) + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(20)) { subject.send(2) } + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(40)) { subject.send(4) } + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(60)) { subject.send(6) } + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(80)) { subject.send(completion: .finished) } + }.expectsCompletion(timeout: 0.2, on: self) + } + + /// Tests failure completion expectations. + func testFailedExpectations() { + Fail(error: .init()).expectsFailure(timeout: 0.2, on: self) + + DeferredPassthrough { (subject) in + subject.send(0) + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(20)) { subject.send(2) } + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(40)) { subject.send(4) } + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(60)) { subject.send(6) } + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(80)) { subject.send(completion: .failure(_CustomError())) } + }.expectsFailure(timeout: 0.2, on: self) + } + + /// Tests one single value emission expectations. + func testSingleValueEmissionExpectations() { + Just(0).expectsOne(timeout: 0.2, on: self) + + DeferredPassthrough { (subject) in + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(20)) { subject.send(0) } + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(80)) { subject.send(completion: .failure(_CustomError())) } + }.expectsFailure(timeout: 0.2, on: self) + } + + /// Tests the reception of all emitted values. + func testAllEmissionExpectations() { + let values = [0, 2, 4, 6, 8] + + let sequenceEmitted = values.publisher.expectsAll(timeout: 0.2, on: self) + XCTAssertEqual(values, sequenceEmitted) + + let subjectEmitted = DeferredPassthrough { (subject) in + for (index, value) in values.enumerated() { + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(index*10)) { subject.send(value) } + } + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(values.count*10)) { subject.send(completion: .finished) } + }.expectsAll(timeout: 0.2, on: self) + XCTAssertEqual(values, subjectEmitted) + } + + /// Tests the reception of at least a given amount of values. + func testAtLeastEmisionExpectations() { + let values = [0, 2, 4, 6, 8] + + let sequenceEmitted = values.publisher.expectsAtLeast(values: 2, timeout: 0.2, on: self) + XCTAssertEqual(.init(values[0..<2]), sequenceEmitted) + + let subjectEmitted = DeferredPassthrough { (subject) in + for (index, value) in values.enumerated() { + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(index*10)) { subject.send(value) } + } + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(values.count*10)) { subject.send(completion: .finished) } + }.expectsAtLeast(values: 2, timeout: 0.2, on: self) + XCTAssertEqual(.init(values[0..<2]), subjectEmitted) + } }