12 Commits

Author SHA1 Message Date
Sergej Jaskiewicz 59183ce0a5 Document the release process 2020-06-12 23:54:21 +03:00
Sergej Jaskiewicz b1f676d273 Bump the version to 0.9.0 2020-06-12 23:54:21 +03:00
Sergej Jaskiewicz b2784a1011 Implement Publishers.Catch and Publishers.TryCatch (#140) 2020-06-11 22:17:16 +03:00
Max Desiatov d67e77c84d Test with Swift 5.2 on Ubuntu 18.04 (#159)
I don't think it makes much sense to test on an older version of Swift on Ubuntu. Since we tested only a single version, I've updated that to the latest available, but let me know if you'd like to test with multiple Swift versions on Linux.

As a sidenote, I hope we could also switch to GitHub Actions in the future. Circle CI seems to be annoyingly slow.
2020-06-06 19:43:20 +01:00
Vuk Radosavljevic d680f09932 Change collection to set in documentation (#151) 2020-04-10 10:16:26 +01:00
Sergej Jaskiewicz 30b5dd4c2f Update for Xcode 11.4 release (#150) 2020-03-28 21:23:57 +03:00
Sergej Jaskiewicz 621f970998 Update to match the behavior in Xcode 11.4 beta 2 SDKs. (#148) 2020-02-26 13:56:18 +03:00
Sergej Jaskiewicz d6b70ad309 Implement the RunLoop scheduler (#131) 2020-02-05 02:11:10 +03:00
Sergej Jaskiewicz 918e9131ad Implement Publishers.SwitchToLatest (#142) 2020-02-04 13:26:17 +03:00
Rob Mayoff 7f7f397062 Add opencombine_lldb.py for better Demand formatting in lldb/Xcode (#146) 2020-01-29 14:57:44 +03:00
Rob Mayoff 3b1437e46c Work around SR-11680 (#145)
The Swift bug report: https://bugs.swift.org/browse/SR-11680

Swift nightly toolchains are available here: https://swift.org/download/

The Swift nightly toolchains cannot build OpenCombine. Here's why:

The COpenCombineHelpers target defines a non-static function
(`opencombine_stop_in_debugger`) in a header file. This function is
emitted in the target's IR, but not in the target's TBD.

Swift nightly toolchains have assertions enabled, so they use the
-validate-tbd-against-ir=missing build setting. This build setting
makes the compiler fail if the TBD doesn't match the IR.

This commit un-inlines `opencombine_stop_in_debugger`, so it
is not emitted in the IR. This stops the TBD validator from
complaining.
2020-01-24 02:17:40 +03:00
Sergej Jaskiewicz 79899f7742 Bump podspec version for OpenCombineFoundation 2020-01-17 17:25:10 +03:00
54 changed files with 4079 additions and 334 deletions
+12 -12
View File
@@ -63,35 +63,35 @@ jobs:
command: |
bash <(curl -s https://codecov.io/bash) -D DerivedData
"Execute compatibility tests on iOS 13.3 (Xcode 11.3.0, Swift 5.1.3)":
"Execute compatibility tests on iOS 13.4 (Xcode 11.4.0, Swift 5.2.0)":
macos:
xcode: "11.3.0"
xcode: "11.4.0"
environment:
SWIFT_VERSION: "5.1.3"
SWIFT_VERSION: "5.2.0"
steps:
- checkout
- run:
name: Generating Xcode project
command: make generate-compatibility-xcodeproj
- run:
name: Building for testing on iOS 13.3 with xcodebuild
name: Building for testing on iOS 13.4 with xcodebuild
command: |
set -o pipefail \
&& xcodebuild build-for-testing \
-scheme OpenCombine-Package \
-destination "platform=iOS Simulator,name=iPhone 11,OS=13.3" \
-destination "platform=iOS Simulator,name=iPhone 11,OS=13.4" \
-derivedDataPath DerivedData \
| tee xcodebuild_build-for-testing.log \
| xcpretty
- store_artifacts:
path: xcodebuild_build-for-testing.log
- run:
name: Testing against Combine on iOS 13.3 with xcodebuild
name: Testing against Combine on iOS 13.4 with xcodebuild
command: |
set -o pipefail \
&& xcodebuild test-without-building \
-scheme OpenCombine-Package \
-destination "platform=iOS Simulator,name=iPhone 11,OS=13.3" \
-destination "platform=iOS Simulator,name=iPhone 11,OS=13.4" \
-derivedDataPath DerivedData \
| tee xcodebuild_test-without-building.log \
| xcpretty --report junit -o build/reports/results.xml
@@ -161,11 +161,11 @@ jobs:
command: |
bash <(curl -s https://codecov.io/bash) -D DerivedData
"Execute tests on Ubuntu 18.04 (Swift 5.1.1)":
"Execute tests on Ubuntu 18.04 (Swift 5.2)":
docker:
- image: swift:5.1.1-bionic
- image: swift:5.2-bionic
environment:
SWIFT_VERSION: "5.1.1"
SWIFT_VERSION: "5.2"
steps:
- checkout
- run:
@@ -259,13 +259,13 @@ workflows:
- "Execute tests on macOS 10.15.0 (Xcode 11.3.0, Swift 5.1.3)"
"OpenCombine: execute compatibility tests":
jobs:
- "Execute compatibility tests on iOS 13.3 (Xcode 11.3.0, Swift 5.1.3)"
- "Execute compatibility tests on iOS 13.4 (Xcode 11.4.0, Swift 5.2.0)"
"OpenCombine: execute tests on iOS":
jobs:
- "Execute tests on iOS 9.3 (Xcode 10.2.1, Swift 5.0.1)"
"OpenCombine: execute tests on Linux":
jobs:
- "Execute tests on Ubuntu 18.04 (Swift 5.1.1)"
- "Execute tests on Ubuntu 18.04 (Swift 5.2)"
"OpenCombine: run SwiftLint and Danger":
jobs:
- "Run SwiftLint and Danger"
+5
View File
@@ -23,6 +23,7 @@ disabled_rules:
- trailing_comma
- type_body_length
- opening_brace
- untyped_error_in_catch
opt_in_rules:
- array_init
@@ -65,6 +66,10 @@ opt_in_rules:
- vertical_whitespace_closing_braces
- yoda_condition
implicit_return:
included:
- closure
line_length:
warning: 90
error: 120
+2 -2
View File
@@ -1,6 +1,6 @@
Pod::Spec.new do |spec|
spec.name = "OpenCombine"
spec.version = "0.8.0"
spec.version = "0.9.0"
spec.summary = "Open source implementation of Apple's Combine framework for processing values over time."
spec.description = <<-DESC
@@ -24,4 +24,4 @@ Pod::Spec.new do |spec|
spec.public_header_files = "Sources/COpenCombineHelpers/include/*.h"
spec.libraries = "c++"
end
end
+3 -3
View File
@@ -1,6 +1,6 @@
Pod::Spec.new do |spec|
spec.name = "OpenCombineDispatch"
spec.version = "0.8.0"
spec.version = "0.9.0"
spec.summary = "OpenCombine + Dispatch interoperability"
spec.description = <<-DESC
@@ -21,5 +21,5 @@ Pod::Spec.new do |spec|
spec.tvos.deployment_target = "9.0"
spec.source_files = "Sources/OpenCombineDispatch/**/*.swift"
spec.dependency "OpenCombine", '>= 0.7'
end
spec.dependency "OpenCombine", '>= 0.8'
end
+3 -3
View File
@@ -1,6 +1,6 @@
Pod::Spec.new do |spec|
spec.name = "OpenCombineFoundation"
spec.version = "0.7.0"
spec.version = "0.9.0"
spec.summary = "OpenCombine + OpenCombineFoundation interoperability"
spec.description = <<-DESC
@@ -21,5 +21,5 @@ Pod::Spec.new do |spec|
spec.tvos.deployment_target = "9.0"
spec.source_files = "Sources/OpenCombineFoundation/**/*.swift"
spec.dependency "OpenCombine", '>= 0.7'
end
spec.dependency "OpenCombine", '>= 0.8'
end
+39 -4
View File
@@ -23,7 +23,7 @@ To add `OpenCombine` to your [SPM](https://swift.org/package-manager/) package,
```swift
dependencies: [
.package(url: "https://github.com/broadwaylamb/OpenCombine.git", from: "0.8.0")
.package(url: "https://github.com/broadwaylamb/OpenCombine.git", from: "0.9.0")
],
targets: [
.target(name: "MyAwesomePackage", dependencies: ["OpenCombine",
@@ -46,9 +46,9 @@ To do so, open Xcode, use **File** → **Swift Packages** → **Add Package Depe
To add `OpenCombine` to a project using [CocoaPods](https://cocoapods.org/), add `OpenCombine` and `OpenCombineDispatch` to the list of target dependencies in your `Podfile`.
```ruby
pod 'OpenCombine', '~> 0.8'
pod 'OpenCombineDispatch', '~> 0.8'
pod 'OpenCombineFoundation', '~> 0.8'
pod 'OpenCombine', '~> 0.9'
pod 'OpenCombineDispatch', '~> 0.9'
pod 'OpenCombineFoundation', '~> 0.9'
```
### Contributing
@@ -69,6 +69,28 @@ Or enable the `-DOPENCOMBINE_COMPATIBILITY_TEST` compiler flag in Xcode's build
> NOTE: Before starting to work on some feature, please consult the [GitHub project](https://github.com/broadwaylamb/OpenCombine/projects/2) to make sure that nobody's already making progress on the same feature! If not, then please create a draft PR to indicate that you're beginning your work.
#### Releasing a new version
1. Create a new branch from master and call it `release/<major>.<minor>.<patch>`.
1. Replace the usages of the old version in `README.md` with the new version (make sure to check the [Swift Package Manager](#swift-package-manager) and [CocoaPods](#cocoapods) sections).
1. Bump the version in `OpenCombine.podspec`, `OpenCombineDispatch.podspec` and `OpenCombineFoundation.podspec`. In the latter two you will also need to set the `spec.dependency "OpenCombine"` property to the **previous** version. Why? Because otherwise the `pod lib lint` command that we run on our regular CI will fail when validating the `OpenCombineDispatch` and `OpenCombineFoundation` podspecs, since the dependencies are not yet in the trunk. If we set the dependencies to the previous version (which is already in the trunk), everything will be fine. This is purely to make the CI work. The clients will not experience any issues, since the version is specified as `>=`.
1. Create a pull request to master for the release branch and make sure the CI passes.
1. Merge the pull request.
1. In the GitHub web interface on the [releases](https://github.com/broadwaylamb/OpenCombine/releases) page, click the **Draft a new release** button.
1. The **Tag version** and **Release title** fields should be filled with the version number.
1. The description of the release should be consistent with the previous releases. It is a good practice to divide the description into several sections: additions, bugfixes, known issues etc. Also, be sure to mention the nicknames of the contributors of the new release.
1. Publish the release.
1. Switch to the master branch and pull the changes.
1. Push the release to CocoaPods trunk. For that, execute the following commands:
```
pod trunk push OpenCombine.podspec --verbose --allow-warnings
pod trunk push OpenCombineDispatch.podspec --verbose --allow-warnings
pod trunk push OpenCombineFoundation.podspec --verbose --allow-warnings
```
Note that you need to be one of the owners of the pod for that.
#### GYB
Some publishers in OpenCombine (like `Publishers.MapKeyPath`, `Publishers.Merge`) exist in several
@@ -89,3 +111,16 @@ GYB template files have the `.gyb` extension. Run `make gyb` to generate Swift c
templates. The generated files are prefixed with `GENERATED-` and are checked into source control. Those
files should never be edited directly. Instead, the `.gyb` template should be edited, and after that the files
should be regenerated using `make gyb`.
#### Debugger Support
The file `opencombine_lldb.py` defines some `lldb` type summaries for easier debugging. These type summaries improve the way `lldb` and Xcode display some OpenCombine values.
To use `opencombine_lldb.py`, figure out its full path. Let's say the full path is `~/projects/OpenCombine/opencombine_lldb.py`. Then the following statement to your `~/.lldbinit` file:
command script import ~/projects/OpenCombine/opencombine_lldb.py
Currently, `opencombine_lldb.py` defines type summaries for these types:
- `Subscribers.Demand`
- That's all for now.
-137
View File
@@ -651,49 +651,6 @@ extension Publisher {
public func merge(with other: Self) -> Publishers.MergeMany<Self>
}
extension Publishers {
/// A publisher that flattens nested publishers.
///
/// Given a publisher that publishes Publishers, the `SwitchToLatest` publisher produces a sequence of events from only the most recent one.
/// For example, given the type `Publisher<Publisher<Data, NSError>, Never>`, calling `switchToLatest()` will result in the type `Publisher<Data, NSError>`. The downstream subscriber sees a continuous stream of values even though they may be coming from different upstream publishers.
public struct SwitchToLatest<P, Upstream> : Publisher where P : Publisher, P == Upstream.Output, Upstream : Publisher, P.Failure == Upstream.Failure {
/// The kind of values published by this publisher.
public typealias Output = P.Output
/// The kind of errors this publisher might publish.
///
/// Use `Never` if this `Publisher` does not publish errors.
public typealias Failure = P.Failure
/// The publisher from which this publisher receives elements.
public let upstream: Upstream
/// Creates a publisher that flattens nested publishers.
///
/// - Parameter upstream: The publisher from which this publisher receives elements.
public init(upstream: Upstream)
/// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)`
///
/// - SeeAlso: `subscribe(_:)`
/// - Parameters:
/// - subscriber: The subscriber to attach to this `Publisher`.
/// once attached it can begin to receive values.
public func receive<S>(subscriber: S) where S : Subscriber, P.Output == S.Input, Upstream.Failure == S.Failure
}
}
extension Publisher where Self.Failure == Self.Output.Failure, Self.Output : Publisher {
/// Flattens the stream of events from multiple upstream publishers to appear as if they were coming from a single stream of events.
///
/// This operator switches the inner publisher as new ones arrive but keeps the outer one constant for downstream subscribers.
/// For example, given the type `Publisher<Publisher<Data, NSError>, Never>`, calling `switchToLatest()` will result in the type `Publisher<Data, NSError>`. The downstream subscriber sees a continuous stream of values even though they may be coming from different upstream publishers.
public func switchToLatest() -> Publishers.SwitchToLatest<Self.Output, Self>
}
extension Publishers {
/// A publisher that attempts to recreate its subscription to a failed upstream publisher.
@@ -1063,100 +1020,6 @@ extension Publisher {
public func zip<P, Q, R, T>(_ publisher1: P, _ publisher2: Q, _ publisher3: R, _ transform: @escaping (Self.Output, P.Output, Q.Output, R.Output) -> T) -> Publishers.Map<Publishers.Zip4<Self, P, Q, R>, T> where P : Publisher, Q : Publisher, R : Publisher, Self.Failure == P.Failure, P.Failure == Q.Failure, Q.Failure == R.Failure
}
extension Publishers {
/// A publisher that handles errors from an upstream publisher by replacing the failed publisher with another publisher.
public struct Catch<Upstream, NewPublisher> : Publisher where Upstream : Publisher, NewPublisher : Publisher, Upstream.Output == NewPublisher.Output {
/// The kind of values published by this publisher.
public typealias Output = Upstream.Output
/// The kind of errors this publisher might publish.
///
/// Use `Never` if this `Publisher` does not publish errors.
public typealias Failure = NewPublisher.Failure
/// The publisher that this publisher receives elements from.
public let upstream: Upstream
/// A closure that accepts the upstream failure as input and returns a publisher to replace the upstream publisher.
public let handler: (Upstream.Failure) -> NewPublisher
/// Creates a publisher that handles errors from an upstream publisher by replacing the failed publisher with another publisher.
///
/// - Parameters:
/// - upstream: The publisher that this publisher receives elements from.
/// - handler: A closure that accepts the upstream failure as input and returns a publisher to replace the upstream publisher.
public init(upstream: Upstream, handler: @escaping (Upstream.Failure) -> NewPublisher)
/// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)`
///
/// - SeeAlso: `subscribe(_:)`
/// - Parameters:
/// - subscriber: The subscriber to attach to this `Publisher`.
/// once attached it can begin to receive values.
public func receive<S>(subscriber: S) where S : Subscriber, NewPublisher.Failure == S.Failure, NewPublisher.Output == S.Input
}
/// A publisher that handles errors from an upstream publisher by replacing the failed publisher with another publisher or optionally producing a new error.
public struct TryCatch<Upstream, NewPublisher> : Publisher where Upstream : Publisher, NewPublisher : Publisher, Upstream.Output == NewPublisher.Output {
/// The kind of values published by this publisher.
public typealias Output = Upstream.Output
/// The kind of errors this publisher might publish.
///
/// Use `Never` if this `Publisher` does not publish errors.
public typealias Failure = Error
public let upstream: Upstream
public let handler: (Upstream.Failure) throws -> NewPublisher
public init(upstream: Upstream, handler: @escaping (Upstream.Failure) throws -> NewPublisher)
/// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)`
///
/// - SeeAlso: `subscribe(_:)`
/// - Parameters:
/// - subscriber: The subscriber to attach to this `Publisher`.
/// once attached it can begin to receive values.
public func receive<S>(subscriber: S) where S : Subscriber, NewPublisher.Output == S.Input, S.Failure == Publishers.TryCatch<Upstream, NewPublisher>.Failure
}
}
extension Publisher {
/// Handles errors from an upstream publisher by replacing it with another publisher.
///
/// The following example replaces any error from the upstream publisher and replaces the upstream with a `Just` publisher. This continues the stream by publishing a single value and completing normally.
/// ```
/// enum SimpleError: Error { case error }
/// let errorPublisher = (0..<10).publisher.tryMap { v -> Int in
/// if v < 5 {
/// return v
/// } else {
/// throw SimpleError.error
/// }
/// }
///
/// let noErrorPublisher = errorPublisher.catch { _ in
/// return Just(100)
/// }
/// ```
/// Backpressure note: This publisher passes through `request` and `cancel` to the upstream. After receiving an error, the publisher sends sends any unfulfilled demand to the new `Publisher`.
/// - Parameter handler: A closure that accepts the upstream failure as input and returns a publisher to replace the upstream publisher.
/// - Returns: A publisher that handles errors from an upstream publisher by replacing the failed publisher with another publisher.
public func `catch`<P>(_ handler: @escaping (Self.Failure) -> P) -> Publishers.Catch<Self, P> where P : Publisher, Self.Output == P.Output
/// Handles errors from an upstream publisher by either replacing it with another publisher or `throw`ing a new error.
///
/// - Parameter handler: A `throw`ing closure that accepts the upstream failure as input and returns a publisher to replace the upstream publisher or if an error is thrown will send the error downstream.
/// - Returns: A publisher that handles errors from an upstream publisher by replacing the failed publisher with another publisher.
public func tryCatch<P>(_ handler: @escaping (Self.Failure) throws -> P) -> Publishers.TryCatch<Self, P> where P : Publisher, Self.Output == P.Output
}
extension Publishers.CombineLatest : Equatable where A : Equatable, B : Equatable {
/// Returns a Boolean value that indicates whether two publishers are equivalent.
-54
View File
@@ -107,57 +107,3 @@ extension OperationQueue : Combine.Scheduler {
get
}
}
extension RunLoop : Combine.Scheduler {
public struct SchedulerTimeType : Swift.Strideable, Swift.Codable, Swift.Hashable {
public var date: Foundation.Date
public init(_ date: Foundation.Date)
public func distance(to other: Foundation.RunLoop.SchedulerTimeType) -> Foundation.RunLoop.SchedulerTimeType.Stride
public func advanced(by n: Foundation.RunLoop.SchedulerTimeType.Stride) -> Foundation.RunLoop.SchedulerTimeType
public struct Stride : Swift.ExpressibleByFloatLiteral, Swift.Comparable, Swift.SignedNumeric, Swift.Codable, Combine.SchedulerTimeIntervalConvertible {
public typealias FloatLiteralType = Foundation.TimeInterval
public typealias IntegerLiteralType = Foundation.TimeInterval
public typealias Magnitude = Foundation.TimeInterval
public var magnitude: Foundation.TimeInterval
public var timeInterval: Foundation.TimeInterval {
get
}
public init(integerLiteral value: Foundation.TimeInterval)
public init(floatLiteral value: Foundation.TimeInterval)
public init(_ timeInterval: Foundation.TimeInterval)
public init?<T>(exactly source: T) where T : Swift.BinaryInteger
public static func < (lhs: Foundation.RunLoop.SchedulerTimeType.Stride, rhs: Foundation.RunLoop.SchedulerTimeType.Stride) -> Swift.Bool
public static func * (lhs: Foundation.RunLoop.SchedulerTimeType.Stride, rhs: Foundation.RunLoop.SchedulerTimeType.Stride) -> Foundation.RunLoop.SchedulerTimeType.Stride
public static func + (lhs: Foundation.RunLoop.SchedulerTimeType.Stride, rhs: Foundation.RunLoop.SchedulerTimeType.Stride) -> Foundation.RunLoop.SchedulerTimeType.Stride
public static func - (lhs: Foundation.RunLoop.SchedulerTimeType.Stride, rhs: Foundation.RunLoop.SchedulerTimeType.Stride) -> Foundation.RunLoop.SchedulerTimeType.Stride
public static func *= (lhs: inout Foundation.RunLoop.SchedulerTimeType.Stride, rhs: Foundation.RunLoop.SchedulerTimeType.Stride)
public static func += (lhs: inout Foundation.RunLoop.SchedulerTimeType.Stride, rhs: Foundation.RunLoop.SchedulerTimeType.Stride)
public static func -= (lhs: inout Foundation.RunLoop.SchedulerTimeType.Stride, rhs: Foundation.RunLoop.SchedulerTimeType.Stride)
public static func seconds(_ s: Swift.Int) -> Foundation.RunLoop.SchedulerTimeType.Stride
public static func seconds(_ s: Swift.Double) -> Foundation.RunLoop.SchedulerTimeType.Stride
public static func milliseconds(_ ms: Swift.Int) -> Foundation.RunLoop.SchedulerTimeType.Stride
public static func microseconds(_ us: Swift.Int) -> Foundation.RunLoop.SchedulerTimeType.Stride
public static func nanoseconds(_ ns: Swift.Int) -> Foundation.RunLoop.SchedulerTimeType.Stride
public init(from decoder: Swift.Decoder) throws
public func encode(to encoder: Swift.Encoder) throws
public static func == (a: Foundation.RunLoop.SchedulerTimeType.Stride, b: Foundation.RunLoop.SchedulerTimeType.Stride) -> Swift.Bool
}
public init(from decoder: Swift.Decoder) throws
public func encode(to encoder: Swift.Encoder) throws
public var hashValue: Swift.Int {
get
}
public func hash(into hasher: inout Swift.Hasher)
}
public struct SchedulerOptions {
}
public func schedule(options: Foundation.RunLoop.SchedulerOptions?, _ action: @escaping () -> Swift.Void)
public func schedule(after date: Foundation.RunLoop.SchedulerTimeType, tolerance: Foundation.RunLoop.SchedulerTimeType.Stride, options: Foundation.RunLoop.SchedulerOptions?, _ action: @escaping () -> Swift.Void)
public func schedule(after date: Foundation.RunLoop.SchedulerTimeType, interval: Foundation.RunLoop.SchedulerTimeType.Stride, tolerance: Foundation.RunLoop.SchedulerTimeType.Stride, options: Foundation.RunLoop.SchedulerOptions?, _ action: @escaping () -> Swift.Void) -> Combine.Cancellable
public var now: Foundation.RunLoop.SchedulerTimeType {
get
}
public var minimumTolerance: Foundation.RunLoop.SchedulerTimeType.Stride {
get
}
}
@@ -11,6 +11,7 @@
#include <cstdlib>
#include <system_error>
#include <pthread.h>
#include <signal.h>
#ifdef __APPLE__
#include <os/lock.h>
@@ -235,4 +236,8 @@ void opencombine_unfair_recursive_lock_dealloc(OpenCombineUnfairRecursiveLock lo
return delete static_cast<PlatformIndependentMutex*>(lock.opaque);
}
void opencombine_stop_in_debugger(void) {
raise(SIGTRAP);
}
} // extern "C"
@@ -9,7 +9,6 @@
#define COPENCOMBINEHELPERS_H
#include <stdint.h>
#include <signal.h>
#if __has_attribute(swift_name)
# define OPENCOMBINE_SWIFT_NAME(_name) __attribute__((swift_name(#_name)))
@@ -17,12 +16,6 @@
# define OPENCOMBINE_SWIFT_NAME(_name)
#endif
#if __has_attribute(always_inline)
# define OPENCOMBINE_ALWAYS_INLINE __attribute__((always_inline))
#else
# define OPENCOMBINE_ALWAYS_INLINE
#endif
#ifdef __cplusplus
extern "C" {
#endif
@@ -77,12 +70,7 @@ void opencombine_unfair_recursive_lock_dealloc(OpenCombineUnfairRecursiveLock lo
#pragma mark - Breakpoint
OPENCOMBINE_ALWAYS_INLINE
inline void opencombine_stop_in_debugger(void) OPENCOMBINE_SWIFT_NAME(__stopInDebugger());
void opencombine_stop_in_debugger(void) {
raise(SIGTRAP);
}
void opencombine_stop_in_debugger(void) OPENCOMBINE_SWIFT_NAME(__stopInDebugger());
#ifdef __cplusplus
} // extern "C"
+1 -1
View File
@@ -57,7 +57,7 @@ extension AnyCancellable {
/// Stores this AnyCancellable in the specified set.
/// Parameters:
/// - collection: The set to store this AnyCancellable.
/// - set: The set to store this AnyCancellable.
public func store(in set: inout Set<AnyCancellable>) {
set.insert(self)
}
+1
View File
@@ -11,6 +11,7 @@ extension Publisher {
///
/// Use `eraseToAnyPublisher()` to expose an instance of `AnyPublisher` to
/// the downstream subscriber, rather than this publishers actual type.
@inlinable
public func eraseToAnyPublisher() -> AnyPublisher<Output, Failure> {
return .init(self)
}
+1 -1
View File
@@ -28,7 +28,7 @@ extension Cancellable {
/// Stores this Cancellable in the specified set.
/// Parameters:
/// - collection: The set to store this Cancellable.
/// - set: The set to store this Cancellable.
public func store(in set: inout Set<AnyCancellable>) {
AnyCancellable(self).store(in: &set)
}
@@ -40,23 +40,21 @@ internal final class SubjectSubscriber<Downstream: Subject>
internal func receive(_ input: Downstream.Output) -> Subscribers.Demand {
lock.lock()
guard let downstreamSubject = downstreamSubject else {
guard let subject = downstreamSubject, upstreamSubscription != nil else {
lock.unlock()
return .none
}
guard upstreamSubscription != nil else { APIViolationValueBeforeSubscription() }
lock.unlock()
downstreamSubject.send(input)
subject.send(input)
return .none
}
internal func receive(completion: Subscribers.Completion<Downstream.Failure>) {
lock.lock()
guard let subject = downstreamSubject else {
guard let subject = downstreamSubject, upstreamSubscription != nil else {
lock.unlock()
return
}
guard upstreamSubscription != nil else { APIViolationUnexpectedCompletion() }
lock.unlock()
subject.send(completion: completion)
downstreamSubject = nil
@@ -87,11 +85,7 @@ internal final class SubjectSubscriber<Downstream: Subject>
internal func cancel() {
lock.lock()
if isCancelled {
lock.unlock()
return
}
guard let subscription = upstreamSubscription else {
guard !isCancelled, let subscription = upstreamSubscription else {
lock.unlock()
return
}
+1 -2
View File
@@ -18,8 +18,7 @@
@propertyWrapper
public struct Published<Value> {
/// Initialize the storage of the `Published` property as well as the corresponding
/// `Publisher`.
@inlinable // trivially forwarding
public init(initialValue: Value) {
self.init(wrappedValue: initialValue)
}
@@ -0,0 +1,620 @@
//
//
// Auto-generated from GYB template. DO NOT EDIT!
//
//
//
//
// Publishers.Catch.swift
//
//
// Created by Sergej Jaskiewicz on 25.12.2019.
//
extension Publisher {
/// Handles errors from an upstream publisher by replacing it with another publisher.
///
/// The following example replaces any error from the upstream publisher and replaces
/// the upstream with a `Just` publisher. This continues the stream by publishing
/// a single value and completing normally.
/// ```
/// enum SimpleError: Error { case error }
/// let errorPublisher = (0..<10).publisher.tryMap { v -> Int in
/// if v < 5 {
/// return v
/// } else {
/// throw SimpleError.error
/// }
/// }
///
/// let noErrorPublisher = errorPublisher.catch { _ in
/// return Just(100)
/// }
/// ```
/// Backpressure note: This publisher passes through `request` and `cancel` to
/// the upstream. After receiving an error, the publisher sends sends any unfulfilled
/// demand to the new `Publisher`.
///
/// - Parameter handler: A closure that accepts the upstream failure as input and
/// returns a publisher to replace the upstream publisher.
/// - Returns: A publisher that handles errors from an upstream publisher by replacing
/// the failed publisher with another publisher.
public func `catch`<NewPublisher: Publisher>(
_ handler: @escaping (Failure) -> NewPublisher
) -> Publishers.Catch<Self, NewPublisher>
where NewPublisher.Output == Output
{
return .init(upstream: self, handler: handler)
}
/// Handles errors from an upstream publisher by either replacing it with another
/// publisher or `throw`ing a new error.
///
/// - Parameter handler: A `throw`ing closure that accepts the upstream failure as
/// input and returns a publisher to replace the upstream publisher or if an error
/// is thrown will send the error downstream.
/// - Returns: A publisher that handles errors from an upstream publisher by replacing
/// the failed publisher with another publisher.
public func tryCatch<NewPublisher: Publisher>(
_ handler: @escaping (Failure) throws -> NewPublisher
) -> Publishers.TryCatch<Self, NewPublisher>
where NewPublisher.Output == Output
{
return .init(upstream: self, handler: handler)
}
}
extension Publishers {
/// A publisher that handles errors from an upstream publisher by replacing the failed
/// publisher with another publisher.
public struct Catch<Upstream: Publisher, NewPublisher: Publisher>: Publisher
where Upstream.Output == NewPublisher.Output
{
public typealias Output = Upstream.Output
public typealias Failure = NewPublisher.Failure
/// The publisher that this publisher receives elements from.
public let upstream: Upstream
/// A closure that accepts the upstream failure as input and returns a publisher
/// to replace the upstream publisher.
public let handler: (Upstream.Failure) -> NewPublisher
/// Creates a publisher that handles errors from an upstream publisher by
/// replacing the failed publisher with another publisher.
///
/// - Parameters:
/// - upstream: The publisher that this publisher receives elements from.
/// - handler: A closure that accepts the upstream failure as input and returns
/// a publisher to replace the upstream publisher.
public init(upstream: Upstream,
handler: @escaping (Upstream.Failure) -> NewPublisher) {
self.upstream = upstream
self.handler = handler
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Downstream.Input == Output, Downstream.Failure == Failure
{
let inner = Inner(downstream: subscriber, handler: handler)
let uncaughtS = Inner.UncaughtS(inner: inner)
upstream.subscribe(uncaughtS)
}
}
/// A publisher that handles errors from an upstream publisher by replacing the failed
/// publisher with another publisher or optionally producing a new error.
public struct TryCatch<Upstream: Publisher, NewPublisher: Publisher>: Publisher
where Upstream.Output == NewPublisher.Output
{
public typealias Output = Upstream.Output
public typealias Failure = Error
public let upstream: Upstream
public let handler: (Upstream.Failure) throws -> NewPublisher
public init(upstream: Upstream,
handler: @escaping (Upstream.Failure) throws -> NewPublisher) {
self.upstream = upstream
self.handler = handler
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Downstream.Input == Output, Downstream.Failure == Failure
{
let inner = Inner(downstream: subscriber, handler: handler)
let uncaughtS = Inner.UncaughtS(inner: inner)
upstream.subscribe(uncaughtS)
}
}
}
extension Publishers.Catch {
private final class Inner<Downstream: Subscriber>
: Subscription,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
where Downstream.Input == Upstream.Output,
Downstream.Failure == NewPublisher.Failure
{
struct UncaughtS: Subscriber,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
{
typealias Input = Upstream.Output
typealias Failure = Upstream.Failure
let inner: Inner
var combineIdentifier: CombineIdentifier { return inner.combineIdentifier }
func receive(subscription: Subscription) {
inner.receivePre(subscription: subscription)
}
func receive(_ input: Input) -> Subscribers.Demand {
return inner.receivePre(input)
}
func receive(completion: Subscribers.Completion<Failure>) {
return inner.receivePre(completion: completion)
}
var description: String { return inner.description }
var customMirror: Mirror { return inner.customMirror }
var playgroundDescription: Any { return description }
}
struct CaughtS: Subscriber,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
{
typealias Input = NewPublisher.Output
typealias Failure = NewPublisher.Failure
let inner: Inner
var combineIdentifier: CombineIdentifier { return inner.combineIdentifier }
func receive(subscription: Subscription) {
inner.receivePost(subscription: subscription)
}
func receive(_ input: Input) -> Subscribers.Demand {
return inner.receivePost(input)
}
func receive(completion: Subscribers.Completion<Failure>) {
inner.receivePost(completion: completion)
}
var description: String { return inner.description }
var customMirror: Mirror { return inner.customMirror }
var playgroundDescription: Any { return description }
}
private enum State {
case pendingPre
case pre(Subscription)
case pendingPost
case post(Subscription)
case cancelled
}
private let lock = UnfairLock.allocate()
private var demand = Subscribers.Demand.none
private var state = State.pendingPre
private let downstream: Downstream
private let handler: (Upstream.Failure) -> NewPublisher
init(downstream: Downstream,
handler: @escaping (Upstream.Failure) -> NewPublisher) {
self.downstream = downstream
self.handler = handler
}
deinit {
lock.deallocate()
}
func receivePre(subscription: Subscription) {
lock.lock()
guard case .pendingPre = state else {
lock.unlock()
subscription.cancel()
return
}
state = .pre(subscription)
lock.unlock()
downstream.receive(subscription: self)
}
func receivePre(_ input: Upstream.Output) -> Subscribers.Demand {
lock.lock()
demand -= 1
lock.unlock()
let newDemand = downstream.receive(input)
lock.lock()
demand += newDemand
lock.unlock()
return newDemand
}
func receivePre(completion: Subscribers.Completion<Upstream.Failure>) {
switch completion {
case .finished:
lock.lock()
switch state {
case .pre:
state = .cancelled
lock.unlock()
downstream.receive(completion: .finished)
case .pendingPre, .pendingPost, .post, .cancelled:
lock.unlock()
}
case .failure(let error):
lock.lock()
switch state {
case .pre:
state = .pendingPost
lock.unlock()
handler(error).subscribe(CaughtS(inner: self))
case .cancelled:
lock.unlock()
case .pendingPre, .post, .pendingPost:
completionBeforeSubscription()
}
}
}
func receivePost(subscription: Subscription) {
lock.lock()
guard case .pendingPost = state else {
lock.unlock()
subscription.cancel()
return
}
state = .post(subscription)
let demand = self.demand
lock.unlock()
if demand > 0 {
subscription.request(demand)
}
}
func receivePost(_ input: NewPublisher.Output) -> Subscribers.Demand {
return downstream.receive(input)
}
func receivePost(completion: Subscribers.Completion<NewPublisher.Failure>) {
lock.lock()
guard case .post = state else {
lock.unlock()
return
}
state = .cancelled
lock.unlock()
downstream.receive(completion: completion)
}
func request(_ demand: Subscribers.Demand) {
demand.assertNonZero()
lock.lock()
switch state {
case .pendingPre:
// The client is only able to call the `request` method after we've sent
// `self` downstream. We only do it in the `receivePre(subscription:)`
// method, after setting `state` to `pre`.
// After that `state` never becomes `pendingPre`.
requestBeforeSubscription()
case let .pre(subscription):
self.demand += demand
lock.unlock()
subscription.request(demand)
case .pendingPost:
self.demand += demand
lock.unlock()
case let .post(subscription):
lock.unlock()
subscription.request(demand)
case .cancelled:
lock.unlock()
}
}
func cancel() {
lock.lock()
switch state {
case let .pre(subscription), let .post(subscription):
state = .cancelled
lock.unlock()
subscription.cancel()
case .pendingPre, .pendingPost, .cancelled:
lock.unlock()
}
}
var description: String { return "Catch" }
var customMirror: Mirror {
let children: [Mirror.Child] = [
("downstream", downstream),
("demand", demand)
]
return Mirror(self, children: children)
}
var playgroundDescription: Any { return description }
}
}
extension Publishers.TryCatch {
private final class Inner<Downstream: Subscriber>
: Subscription,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
where Downstream.Input == Upstream.Output,
Downstream.Failure == Error
{
struct UncaughtS: Subscriber,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
{
typealias Input = Upstream.Output
typealias Failure = Upstream.Failure
let inner: Inner
var combineIdentifier: CombineIdentifier { return inner.combineIdentifier }
func receive(subscription: Subscription) {
inner.receivePre(subscription: subscription)
}
func receive(_ input: Input) -> Subscribers.Demand {
return inner.receivePre(input)
}
func receive(completion: Subscribers.Completion<Failure>) {
return inner.receivePre(completion: completion)
}
var description: String { return inner.description }
var customMirror: Mirror { return inner.customMirror }
var playgroundDescription: Any { return description }
}
struct CaughtS: Subscriber,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
{
typealias Input = NewPublisher.Output
typealias Failure = NewPublisher.Failure
let inner: Inner
var combineIdentifier: CombineIdentifier { return inner.combineIdentifier }
func receive(subscription: Subscription) {
inner.receivePost(subscription: subscription)
}
func receive(_ input: Input) -> Subscribers.Demand {
return inner.receivePost(input)
}
func receive(completion: Subscribers.Completion<Failure>) {
inner.receivePost(completion: completion)
}
var description: String { return inner.description }
var customMirror: Mirror { return inner.customMirror }
var playgroundDescription: Any { return description }
}
private enum State {
case pendingPre
case pre(Subscription)
case pendingPost
case post(Subscription)
case cancelled
}
private let lock = UnfairLock.allocate()
private var demand = Subscribers.Demand.none
private var state = State.pendingPre
private let downstream: Downstream
private let handler: (Upstream.Failure) throws -> NewPublisher
init(downstream: Downstream,
handler: @escaping (Upstream.Failure) throws -> NewPublisher) {
self.downstream = downstream
self.handler = handler
}
deinit {
lock.deallocate()
}
func receivePre(subscription: Subscription) {
lock.lock()
guard case .pendingPre = state else {
lock.unlock()
subscription.cancel()
return
}
state = .pre(subscription)
lock.unlock()
downstream.receive(subscription: self)
}
func receivePre(_ input: Upstream.Output) -> Subscribers.Demand {
lock.lock()
demand -= 1
lock.unlock()
let newDemand = downstream.receive(input)
lock.lock()
demand += newDemand
lock.unlock()
return newDemand
}
func receivePre(completion: Subscribers.Completion<Upstream.Failure>) {
switch completion {
case .finished:
lock.lock()
switch state {
case .pre:
state = .cancelled
lock.unlock()
downstream.receive(completion: .finished)
case .pendingPre, .pendingPost, .post, .cancelled:
lock.unlock()
}
case .failure(let error):
lock.lock()
switch state {
case .pre:
state = .pendingPost
lock.unlock()
do {
try handler(error).subscribe(CaughtS(inner: self))
} catch let anotherError {
lock.lock()
state = .cancelled
lock.unlock()
downstream.receive(completion: .failure(anotherError))
}
case .cancelled:
lock.unlock()
case .pendingPre, .post, .pendingPost:
completionBeforeSubscription()
}
}
}
func receivePost(subscription: Subscription) {
lock.lock()
guard case .pendingPost = state else {
lock.unlock()
subscription.cancel()
return
}
state = .post(subscription)
let demand = self.demand
lock.unlock()
if demand > 0 {
subscription.request(demand)
}
}
func receivePost(_ input: NewPublisher.Output) -> Subscribers.Demand {
return downstream.receive(input)
}
func receivePost(completion: Subscribers.Completion<NewPublisher.Failure>) {
lock.lock()
guard case .post = state else {
lock.unlock()
return
}
state = .cancelled
lock.unlock()
downstream.receive(completion: completion.eraseError())
}
func request(_ demand: Subscribers.Demand) {
demand.assertNonZero()
lock.lock()
switch state {
case .pendingPre:
// The client is only able to call the `request` method after we've sent
// `self` downstream. We only do it in the `receivePre(subscription:)`
// method, after setting `state` to `pre`.
// After that `state` never becomes `pendingPre`.
requestBeforeSubscription()
case let .pre(subscription):
self.demand += demand
lock.unlock()
subscription.request(demand)
case .pendingPost:
self.demand += demand
lock.unlock()
case let .post(subscription):
lock.unlock()
subscription.request(demand)
case .cancelled:
lock.unlock()
}
}
func cancel() {
lock.lock()
switch state {
case let .pre(subscription), let .post(subscription):
state = .cancelled
lock.unlock()
subscription.cancel()
case .pendingPre, .pendingPost, .cancelled:
lock.unlock()
}
}
var description: String { return "TryCatch" }
var customMirror: Mirror {
let children: [Mirror.Child] = [
("downstream", downstream),
("demand", demand)
]
return Mirror(self, children: children)
}
var playgroundDescription: Any { return description }
}
}
private func completionBeforeSubscription(file: StaticString = #file,
line: UInt = #line) -> Never {
fatalError("Unexpected state: received completion but do not have subscription",
file: file,
line: line)
}
private func requestBeforeSubscription(file: StaticString = #file,
line: UInt = #line) -> Never {
fatalError("Unexpected state: request before subscription sent",
file: file,
line: line)
}
@@ -247,14 +247,24 @@ extension Publishers.Buffer {
var upstreamDemand = Subscribers.Demand.none
lock.lock()
while true {
guard case let .subscribed(buffer, downstream, _) = state,
downstreamDemand > 0 else {
guard case let .subscribed(buffer, downstream, _) = state else {
lock.unlock()
return upstreamDemand
}
if values.isEmpty {
if let completion = terminal {
if downstreamDemand > 0 {
if values.isEmpty {
if let completion = terminal {
state = .terminal
lock.unlock()
downstream.receive(completion: completion)
} else {
lock.unlock()
}
return upstreamDemand
}
} else {
if let completion = terminal, case .failure = completion {
state = .terminal
lock.unlock()
downstream.receive(completion: completion)
@@ -0,0 +1,401 @@
${template_header}
//
// Publishers.Catch.swift
//
//
// Created by Sergej Jaskiewicz on 25.12.2019.
//
%{
instantiations = ['Catch', 'TryCatch']
}%
extension Publisher {
/// Handles errors from an upstream publisher by replacing it with another publisher.
///
/// The following example replaces any error from the upstream publisher and replaces
/// the upstream with a `Just` publisher. This continues the stream by publishing
/// a single value and completing normally.
/// ```
/// enum SimpleError: Error { case error }
/// let errorPublisher = (0..<10).publisher.tryMap { v -> Int in
/// if v < 5 {
/// return v
/// } else {
/// throw SimpleError.error
/// }
/// }
///
/// let noErrorPublisher = errorPublisher.catch { _ in
/// return Just(100)
/// }
/// ```
/// Backpressure note: This publisher passes through `request` and `cancel` to
/// the upstream. After receiving an error, the publisher sends sends any unfulfilled
/// demand to the new `Publisher`.
///
/// - Parameter handler: A closure that accepts the upstream failure as input and
/// returns a publisher to replace the upstream publisher.
/// - Returns: A publisher that handles errors from an upstream publisher by replacing
/// the failed publisher with another publisher.
public func `catch`<NewPublisher: Publisher>(
_ handler: @escaping (Failure) -> NewPublisher
) -> Publishers.Catch<Self, NewPublisher>
where NewPublisher.Output == Output
{
return .init(upstream: self, handler: handler)
}
/// Handles errors from an upstream publisher by either replacing it with another
/// publisher or `throw`ing a new error.
///
/// - Parameter handler: A `throw`ing closure that accepts the upstream failure as
/// input and returns a publisher to replace the upstream publisher or if an error
/// is thrown will send the error downstream.
/// - Returns: A publisher that handles errors from an upstream publisher by replacing
/// the failed publisher with another publisher.
public func tryCatch<NewPublisher: Publisher>(
_ handler: @escaping (Failure) throws -> NewPublisher
) -> Publishers.TryCatch<Self, NewPublisher>
where NewPublisher.Output == Output
{
return .init(upstream: self, handler: handler)
}
}
extension Publishers {
/// A publisher that handles errors from an upstream publisher by replacing the failed
/// publisher with another publisher.
public struct Catch<Upstream: Publisher, NewPublisher: Publisher>: Publisher
where Upstream.Output == NewPublisher.Output
{
public typealias Output = Upstream.Output
public typealias Failure = NewPublisher.Failure
/// The publisher that this publisher receives elements from.
public let upstream: Upstream
/// A closure that accepts the upstream failure as input and returns a publisher
/// to replace the upstream publisher.
public let handler: (Upstream.Failure) -> NewPublisher
/// Creates a publisher that handles errors from an upstream publisher by
/// replacing the failed publisher with another publisher.
///
/// - Parameters:
/// - upstream: The publisher that this publisher receives elements from.
/// - handler: A closure that accepts the upstream failure as input and returns
/// a publisher to replace the upstream publisher.
public init(upstream: Upstream,
handler: @escaping (Upstream.Failure) -> NewPublisher) {
self.upstream = upstream
self.handler = handler
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Downstream.Input == Output, Downstream.Failure == Failure
{
let inner = Inner(downstream: subscriber, handler: handler)
let uncaughtS = Inner.UncaughtS(inner: inner)
upstream.subscribe(uncaughtS)
}
}
/// A publisher that handles errors from an upstream publisher by replacing the failed
/// publisher with another publisher or optionally producing a new error.
public struct TryCatch<Upstream: Publisher, NewPublisher: Publisher>: Publisher
where Upstream.Output == NewPublisher.Output
{
public typealias Output = Upstream.Output
public typealias Failure = Error
public let upstream: Upstream
public let handler: (Upstream.Failure) throws -> NewPublisher
public init(upstream: Upstream,
handler: @escaping (Upstream.Failure) throws -> NewPublisher) {
self.upstream = upstream
self.handler = handler
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Downstream.Input == Output, Downstream.Failure == Failure
{
let inner = Inner(downstream: subscriber, handler: handler)
let uncaughtS = Inner.UncaughtS(inner: inner)
upstream.subscribe(uncaughtS)
}
}
}
% for instantiation in instantiations:
% throws_modifier = ' throws' if instantiation == 'TryCatch' else ''
extension Publishers.${instantiation} {
private final class Inner<Downstream: Subscriber>
: Subscription,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
where Downstream.Input == Upstream.Output,
% if instantiation == 'Catch':
Downstream.Failure == NewPublisher.Failure
% else:
Downstream.Failure == Error
% end
{
struct UncaughtS: Subscriber,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
{
typealias Input = Upstream.Output
typealias Failure = Upstream.Failure
let inner: Inner
var combineIdentifier: CombineIdentifier { return inner.combineIdentifier }
func receive(subscription: Subscription) {
inner.receivePre(subscription: subscription)
}
func receive(_ input: Input) -> Subscribers.Demand {
return inner.receivePre(input)
}
func receive(completion: Subscribers.Completion<Failure>) {
return inner.receivePre(completion: completion)
}
var description: String { return inner.description }
var customMirror: Mirror { return inner.customMirror }
var playgroundDescription: Any { return description }
}
struct CaughtS: Subscriber,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
{
typealias Input = NewPublisher.Output
typealias Failure = NewPublisher.Failure
let inner: Inner
var combineIdentifier: CombineIdentifier { return inner.combineIdentifier }
func receive(subscription: Subscription) {
inner.receivePost(subscription: subscription)
}
func receive(_ input: Input) -> Subscribers.Demand {
return inner.receivePost(input)
}
func receive(completion: Subscribers.Completion<Failure>) {
inner.receivePost(completion: completion)
}
var description: String { return inner.description }
var customMirror: Mirror { return inner.customMirror }
var playgroundDescription: Any { return description }
}
private enum State {
case pendingPre
case pre(Subscription)
case pendingPost
case post(Subscription)
case cancelled
}
private let lock = UnfairLock.allocate()
private var demand = Subscribers.Demand.none
private var state = State.pendingPre
private let downstream: Downstream
private let handler: (Upstream.Failure)${throws_modifier} -> NewPublisher
init(downstream: Downstream,
handler: @escaping (Upstream.Failure)${throws_modifier} -> NewPublisher) {
self.downstream = downstream
self.handler = handler
}
deinit {
lock.deallocate()
}
func receivePre(subscription: Subscription) {
lock.lock()
guard case .pendingPre = state else {
lock.unlock()
subscription.cancel()
return
}
state = .pre(subscription)
lock.unlock()
downstream.receive(subscription: self)
}
func receivePre(_ input: Upstream.Output) -> Subscribers.Demand {
lock.lock()
demand -= 1
lock.unlock()
let newDemand = downstream.receive(input)
lock.lock()
demand += newDemand
lock.unlock()
return newDemand
}
func receivePre(completion: Subscribers.Completion<Upstream.Failure>) {
switch completion {
case .finished:
lock.lock()
switch state {
case .pre:
state = .cancelled
lock.unlock()
downstream.receive(completion: .finished)
case .pendingPre, .pendingPost, .post, .cancelled:
lock.unlock()
}
case .failure(let error):
lock.lock()
switch state {
case .pre:
state = .pendingPost
lock.unlock()
% if instantiation == 'Catch':
handler(error).subscribe(CaughtS(inner: self))
% else:
do {
try handler(error).subscribe(CaughtS(inner: self))
} catch let anotherError {
lock.lock()
state = .cancelled
lock.unlock()
downstream.receive(completion: .failure(anotherError))
}
% end
case .cancelled:
lock.unlock()
case .pendingPre, .post, .pendingPost:
completionBeforeSubscription()
}
}
}
func receivePost(subscription: Subscription) {
lock.lock()
guard case .pendingPost = state else {
lock.unlock()
subscription.cancel()
return
}
state = .post(subscription)
let demand = self.demand
lock.unlock()
if demand > 0 {
subscription.request(demand)
}
}
func receivePost(_ input: NewPublisher.Output) -> Subscribers.Demand {
return downstream.receive(input)
}
func receivePost(completion: Subscribers.Completion<NewPublisher.Failure>) {
lock.lock()
guard case .post = state else {
lock.unlock()
return
}
state = .cancelled
lock.unlock()
% if instantiation == 'Catch':
downstream.receive(completion: completion)
% else:
downstream.receive(completion: completion.eraseError())
% end
}
func request(_ demand: Subscribers.Demand) {
demand.assertNonZero()
lock.lock()
switch state {
case .pendingPre:
// The client is only able to call the `request` method after we've sent
// `self` downstream. We only do it in the `receivePre(subscription:)`
// method, after setting `state` to `pre`.
// After that `state` never becomes `pendingPre`.
requestBeforeSubscription()
case let .pre(subscription):
self.demand += demand
lock.unlock()
subscription.request(demand)
case .pendingPost:
self.demand += demand
lock.unlock()
case let .post(subscription):
lock.unlock()
subscription.request(demand)
case .cancelled:
lock.unlock()
}
}
func cancel() {
lock.lock()
switch state {
case let .pre(subscription), let .post(subscription):
state = .cancelled
lock.unlock()
subscription.cancel()
case .pendingPre, .pendingPost, .cancelled:
lock.unlock()
}
}
var description: String { return "${instantiation}" }
var customMirror: Mirror {
let children: [Mirror.Child] = [
("downstream", downstream),
("demand", demand)
]
return Mirror(self, children: children)
}
var playgroundDescription: Any { return description }
}
}
% end
private func completionBeforeSubscription(file: StaticString = #file,
line: UInt = #line) -> Never {
fatalError("Unexpected state: received completion but do not have subscription",
file: file,
line: line)
}
private func requestBeforeSubscription(file: StaticString = #file,
line: UInt = #line) -> Never {
fatalError("Unexpected state: request before subscription sent",
file: file,
line: line)
}
@@ -110,8 +110,8 @@ extension Publishers {
where Suffix.Failure == Downstream.Failure, Suffix.Output == Downstream.Input
{
let inner = Inner(downstream: subscriber, suffix: suffix)
prefix.subscribe(inner)
subscriber.receive(subscription: inner)
prefix.subscribe(inner)
}
}
}
@@ -65,9 +65,9 @@ extension Publishers {
Other.Failure == Downstream.Failure
{
let inner = Inner(downstream: subscriber)
subscriber.receive(subscription: inner)
other.subscribe(Inner.OtherSubscriber(inner: inner))
upstream.subscribe(inner)
subscriber.receive(subscription: inner)
}
}
}
@@ -78,9 +78,8 @@ extension Publishers.FlatMap {
/// acquired.
private var outerSubscription: Subscription?
// Must be recursive lock. Probably a bug in Combine.
/// The lock for requesting from `outerSubscription`.
private let outerLock = UnfairLock.allocate()
private let outerLock = UnfairRecursiveLock.allocate()
/// The lock for modifying the state. All mutable state here should be
/// read and modified with this lock acquired.
@@ -129,8 +129,8 @@ extension Publishers.ReceiveOn {
return .none
}
lock.unlock()
receiveOn.scheduler.schedule(options: receiveOn.options) { [weak self] in
self?.scheduledReceive(input, downstream: downstream)
receiveOn.scheduler.schedule(options: receiveOn.options) {
self.scheduledReceive(input, downstream: downstream)
}
return .none
}
@@ -159,8 +159,8 @@ extension Publishers.ReceiveOn {
}
state = .terminal
lock.unlock()
receiveOn.scheduler.schedule(options: receiveOn.options) { [weak self] in
self?.scheduledReceive(completion: completion, downstream: downstream)
receiveOn.scheduler.schedule(options: receiveOn.options) {
self.scheduledReceive(completion: completion, downstream: downstream)
}
}
@@ -0,0 +1,336 @@
//
// Publishers.SwitchToLatest.swift
//
//
// Created by Sergej Jaskiewicz on 07.01.2020.
//
extension Publisher where Output: Publisher, Output.Failure == Failure {
/// Flattens the stream of events from multiple upstream publishers to appear as if
/// they were coming from a single stream of events.
///
/// This operator switches the inner publisher as new ones arrive but keeps the outer
/// one constant for downstream subscribers.
/// For example, given the type `Publisher<Publisher<Data, NSError>, Never>`,
/// calling `switchToLatest()` will result in the type `Publisher<Data, NSError>`.
/// The downstream subscriber sees a continuous stream of values even though they may
/// be coming from different upstream publishers.
public func switchToLatest() -> Publishers.SwitchToLatest<Output, Self> {
return .init(upstream: self)
}
}
extension Publishers {
/// A publisher that flattens nested publishers.
///
/// Given a publisher that publishes Publishers, the `SwitchToLatest` publisher
/// produces a sequence of events from only the most recent one.
///
/// For example, given the type `Publisher<Publisher<Data, NSError>, Never>`,
/// calling `switchToLatest()` will result in the type `Publisher<Data, NSError>`.
/// The downstream subscriber sees a continuous stream of values even though they may
/// be coming from different upstream publishers.
public struct SwitchToLatest<NestedPublisher: Publisher, Upstream: Publisher>
: Publisher
where Upstream.Output == NestedPublisher,
Upstream.Failure == NestedPublisher.Failure
{
public typealias Output = NestedPublisher.Output
public typealias Failure = NestedPublisher.Failure
/// The publisher from which this publisher receives elements.
public let upstream: Upstream
/// Creates a publisher that flattens nested publishers.
///
/// - Parameter upstream: The publisher from which this publisher receives
/// elements.
public init(upstream: Upstream) {
self.upstream = upstream
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Downstream.Input == Output, Downstream.Failure == Failure
{
let outer = Outer(downstream: subscriber)
subscriber.receive(subscription: outer)
upstream.subscribe(outer)
}
}
}
extension Publishers.SwitchToLatest {
fileprivate final class Outer<Downstream: Subscriber>
: Subscriber,
Subscription,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
where Downstream.Input == NestedPublisher.Output,
Downstream.Failure == Upstream.Failure
{
typealias Input = Upstream.Output
typealias Failure = Upstream.Failure
private let downstream: Downstream
private var outerSubscription: Subscription?
private var currentInnerSubscription: Subscription?
private var currentInnerIndex: UInt64 = 0
private var nextInnerIndex: UInt64 = 1
private let lock = UnfairLock.allocate()
private let downstreamLock = UnfairRecursiveLock.allocate()
private var cancelled = false
private var finished = false
private var sentCompletion = false
private var awaitingInnerSubscription = false
private var downstreamDemand = Subscribers.Demand.none
init(downstream: Downstream) {
self.downstream = downstream
}
deinit {
lock.deallocate()
downstreamLock.deallocate()
}
func receive(subscription: Subscription) {
lock.lock()
guard outerSubscription == nil && !cancelled else {
lock.unlock()
subscription.cancel()
return
}
outerSubscription = subscription
lock.unlock()
subscription.request(.unlimited)
}
func receive(_ input: Input) -> Subscribers.Demand {
lock.lock()
if cancelled || finished {
lock.unlock()
return .none
}
if let currentInnerSubscription = self.currentInnerSubscription {
self.currentInnerSubscription = nil
lock.unlock()
currentInnerSubscription.cancel()
lock.lock()
}
let index = nextInnerIndex
currentInnerIndex = index
nextInnerIndex += 1
awaitingInnerSubscription = true
lock.unlock()
input.subscribe(Side(inner: self, index: index))
return .none
}
func receive(completion: Subscribers.Completion<Failure>) {
lock.lock()
outerSubscription = nil
finished = true
if cancelled {
lock.unlock()
return
}
switch completion {
case .finished:
if awaitingInnerSubscription {
lock.unlock()
return
}
if currentInnerSubscription == nil {
sentCompletion = true
lock.unlock()
downstreamLock.lock()
downstream.receive(completion: completion)
downstreamLock.unlock()
} else {
lock.unlock()
}
case .failure:
let currentInnerSubscription = self.currentInnerSubscription
self.currentInnerSubscription = nil
sentCompletion = true
lock.unlock()
currentInnerSubscription?.cancel()
downstreamLock.lock()
downstream.receive(completion: completion)
downstreamLock.unlock()
}
}
func request(_ demand: Subscribers.Demand) {
demand.assertNonZero()
lock.lock()
downstreamDemand += demand
if let currentInnerSubscription = self.currentInnerSubscription {
lock.unlock()
currentInnerSubscription.request(demand)
} else {
lock.unlock()
}
}
func cancel() {
lock.lock()
cancelled = true
let currentInnerSubscription = self.currentInnerSubscription
self.currentInnerSubscription = nil
let outerSubscription = self.outerSubscription
self.outerSubscription = nil
lock.unlock()
currentInnerSubscription?.cancel()
outerSubscription?.cancel()
}
var description: String { return "SwitchToLatest" }
var customMirror: Mirror {
return Mirror(self, children: EmptyCollection())
}
var playgroundDescription: Any { return description }
private func receiveInner(subscription: Subscription, _ index: UInt64) {
lock.lock()
guard currentInnerIndex == index &&
!cancelled &&
currentInnerSubscription == nil else {
lock.unlock()
subscription.cancel()
return
}
currentInnerSubscription = subscription
awaitingInnerSubscription = false
let downstreamDemand = self.downstreamDemand
lock.unlock()
if downstreamDemand > 0 {
subscription.request(downstreamDemand)
}
}
private func receiveInner(_ input: NestedPublisher.Output,
_ index: UInt64) -> Subscribers.Demand {
lock.lock()
guard currentInnerIndex == index && !cancelled else {
lock.unlock()
return .none
}
// This will crash if we don't have any demand yet.
// Combine crashes here too.
downstreamDemand -= 1
lock.unlock()
downstreamLock.lock()
let newDemand = downstream.receive(input)
downstreamLock.unlock()
if newDemand > 0 {
lock.lock()
downstreamDemand += newDemand
lock.unlock()
}
return newDemand
}
private func receiveInner(completion: Subscribers.Completion<Failure>,
_ index: UInt64) {
lock.lock()
guard currentInnerIndex == index && !cancelled else {
lock.unlock()
return
}
precondition(!awaitingInnerSubscription, "Unexpected completion")
currentInnerSubscription = nil
switch completion {
case .finished:
if sentCompletion || !finished {
lock.unlock()
return
}
sentCompletion = true
lock.unlock()
downstreamLock.lock()
downstream.receive(completion: completion)
downstreamLock.unlock()
case .failure:
if sentCompletion {
lock.unlock()
return
}
cancelled = true
let outerSubscription = self.outerSubscription
self.outerSubscription = nil
sentCompletion = true
lock.unlock()
outerSubscription?.cancel()
downstreamLock.lock()
downstream.receive(completion: completion)
downstreamLock.unlock()
}
}
}
}
extension Publishers.SwitchToLatest.Outer {
private struct Side
: Subscriber,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
{
typealias Input = NestedPublisher.Output
typealias Failure = NestedPublisher.Failure
typealias Outer =
Publishers.SwitchToLatest<NestedPublisher, Upstream>.Outer<Downstream>
private let index: UInt64
private let outer: Outer
let combineIdentifier = CombineIdentifier()
init(inner: Outer, index: UInt64) {
self.index = index
self.outer = inner
}
func receive(subscription: Subscription) {
outer.receiveInner(subscription: subscription, index)
}
func receive(_ input: Input) -> Subscribers.Demand {
return outer.receiveInner(input, index)
}
func receive(completion: Subscribers.Completion<Failure>) {
outer.receiveInner(completion: completion, index)
}
var description: String { return "SwitchToLatest" }
var customMirror: Mirror {
let children = CollectionOfOne<Mirror.Child>(
("parentSubscription", outer.combineIdentifier)
)
return Mirror(self, children: children)
}
var playgroundDescription: Any { return description }
}
}
@@ -61,6 +61,7 @@ extension Subscribers {
}
}
/// Returns the result of adding two demands.
/// When adding any value to `.unlimited`, the result is `.unlimited`.
@inline(__always)
@inlinable
@@ -77,6 +78,7 @@ extension Subscribers {
}
}
/// Adds two demands, and assigns the result to the first demand.
/// When adding any value to `.unlimited`, the result is `.unlimited`.
@inline(__always)
@inlinable
@@ -85,6 +87,7 @@ extension Subscribers {
lhs = lhs + rhs
}
/// Returns the result of adding an integer to a demand.
/// When adding any value to` .unlimited`, the result is `.unlimited`.
@inline(__always)
@inlinable
@@ -96,6 +99,7 @@ extension Subscribers {
return isOverflow ? .unlimited : .max(sum)
}
/// Adds an integer to a demand, and assigns the result to the demand.
/// When adding any value to `.unlimited`, the result is `.unlimited`.
@inline(__always)
@inlinable
@@ -103,6 +107,9 @@ extension Subscribers {
lhs = lhs + rhs
}
/// Returns the result of multiplying a demand by an integer.
/// When multiplying any value by `.unlimited`, the result is `.unlimited`. If
/// the multiplication operation overflows, the result is `.unlimited`.
public static func * (lhs: Demand, rhs: Int) -> Demand {
if lhs == .unlimited {
return .unlimited
@@ -112,12 +119,16 @@ extension Subscribers {
return isOverflow ? .unlimited : .max(product)
}
/// Multiplies a demand by an integer, and assigns the result to the demand.
/// When multiplying any value by `.unlimited`, the result is `.unlimited`. If
/// the multiplication operation overflows, the result is `.unlimited`.
@inline(__always)
@inlinable
public static func *= (lhs: inout Demand, rhs: Int) {
lhs = lhs * rhs
}
/// Returns the result of subtracting one demand from another.
/// When subtracting any value (including `.unlimited`) from `.unlimited`,
/// the result is still `.unlimited`. Subtracting `.unlimited` from any value
/// (except `.unlimited`) results in `.max(0)`. A negative demand is not possible;
@@ -137,6 +148,7 @@ extension Subscribers {
}
}
/// Subtracts one demand from another, and assigns the result to the first demand.
/// When subtracting any value (including `.unlimited`) from `.unlimited`,
/// the result is still `.unlimited`. Subtracting unlimited from any value
/// (except `.unlimited`) results in `.max(0)`. A negative demand is not possible;
@@ -148,6 +160,7 @@ extension Subscribers {
lhs = lhs - rhs
}
/// Returns the result of subtracting an integer from a demand.
/// When subtracting any value from `.unlimited`, the result is still
/// `.unlimited`.
/// A negative demand is not possible; any operation that would result in
@@ -164,6 +177,7 @@ extension Subscribers {
return isOverflow ? .none : .max(difference)
}
/// Subtracts an integer from a demand, and assigns the result to the demand.
/// When subtracting any value from `.unlimited,` the result is still
/// `.unlimited`.
/// A negative demand is not possible; any operation that would result in
@@ -175,6 +189,10 @@ extension Subscribers {
lhs = lhs - rhs
}
/// Returns a Boolean that indicates whether the demand requests more than
/// the given number of elements.
/// If `lhs` is `.unlimited`, then the result is always `true`.
/// Otherwise, the operator compares the demands `max` value to `rhs`.
@inline(__always)
@inlinable
public static func > (lhs: Demand, rhs: Int) -> Bool {
@@ -185,6 +203,10 @@ extension Subscribers {
}
}
/// Returns a Boolean that indicates whether the first demand requests more or
/// the same number of elements as the second.
/// If `lhs` is `.unlimited`, then the result is always `true`.
/// Otherwise, the operator compares the demands `max` value to `rhs`.
@inline(__always)
@inlinable
public static func >= (lhs: Demand, rhs: Int) -> Bool {
@@ -195,6 +217,10 @@ extension Subscribers {
}
}
/// Returns a Boolean that indicates a given number of elements is greater than
/// the maximum specified by the demand.
/// If `rhs` is `.unlimited`, then the result is always `false`.
/// Otherwise, the operator compares the demands `max` value to `lhs`.
@inline(__always)
@inlinable
public static func > (lhs: Int, rhs: Demand) -> Bool {
@@ -205,6 +231,10 @@ extension Subscribers {
}
}
/// Returns a Boolean that indicates a given number of elements is greater than
/// or equal to the maximum specified by the demand.
/// If `rhs` is `.unlimited`, then the result is always `false`.
/// Otherwise, the operator compares the demands `max` value to `lhs`.
@inline(__always)
@inlinable
public static func >= (lhs: Int, rhs: Demand) -> Bool {
@@ -215,6 +245,10 @@ extension Subscribers {
}
}
/// Returns a Boolean that indicates whether the demand requests fewer than
/// the given number of elements.
/// If `lhs` is `.unlimited`, then the result is always `false`.
/// Otherwise, the operator compares the demands `max` value to `rhs`.
@inline(__always)
@inlinable
public static func < (lhs: Demand, rhs: Int) -> Bool {
@@ -225,6 +259,10 @@ extension Subscribers {
}
}
/// Returns a Boolean that indicates a given number of elements is less than
/// the maximum specified by the demand.
/// If `rhs` is `.unlimited`, then the result is always `true`.
/// Otherwise, the operator compares the demands `max` value to `lhs`.
@inline(__always)
@inlinable
public static func < (lhs: Int, rhs: Demand) -> Bool {
@@ -235,6 +273,10 @@ extension Subscribers {
}
}
/// Returns a Boolean that indicates whether the demand requests fewer or
/// the same number of elements as the given integer.
/// If `lhs` is `.unlimited`, then the result is always `false`.
/// Otherwise, the operator compares the demands `max` value to `rhs`.
@inline(__always)
@inlinable
public static func <= (lhs: Demand, rhs: Int) -> Bool {
@@ -245,6 +287,10 @@ extension Subscribers {
}
}
/// Returns a Boolean value that indicates a given number of elements is less
/// than or equal the maximum specified by the demand.
/// If `rhs` is `.unlimited`, then the result is always `true`.
/// Otherwise, the operator compares the demands `max` value to `lhs`.
@inline(__always)
@inlinable
public static func <= (lhs: Int, rhs: Demand) -> Bool {
@@ -255,9 +301,12 @@ extension Subscribers {
}
}
/// Returns a Boolean value that indicates whether the first demand requests fewer
/// elements than the second.
/// If both sides are `.unlimited`, the result is always `false`.
/// If `lhs` is `.unlimited`, then the result is always `false`.
/// If `rhs` is `.unlimited` then the result is `false` iff `lhs` is `.unlimited`
/// Otherwise, the two `.max` values are compared.
/// If `rhs` is `.unlimited`, then the result is always `true`.
/// Otherwise, this operator compares the demands `max` values.
@inline(__always)
@inlinable
public static func < (lhs: Demand, rhs: Demand) -> Bool {
@@ -271,6 +320,12 @@ extension Subscribers {
}
}
/// Returns a Boolean value that indicates whether the first demand requests fewer
/// or the same number of elements as the second.
/// If both sides are `.unlimited`, the result is always `true`.
/// If `lhs` is `.unlimited`, then the result is always `false`.
/// If `rhs` is `.unlimited` then the result is always `true`.
/// Otherwise, this operator compares the demands `max` values.
@inline(__always)
@inlinable
public static func <= (lhs: Demand, rhs: Demand) -> Bool {
@@ -286,6 +341,12 @@ extension Subscribers {
}
}
/// Returns a Boolean that indicates whether the first demand requests more or
/// the same number of elements as the second.
/// If both sides are `.unlimited`, the result is always `false`.
/// If `lhs` is `.unlimited`, then the result is always `true`.
/// If rhs is `.unlimited` then the result is always `false`.
/// Otherwise, this operator compares the demands `max` values.
@inline(__always)
@inlinable
public static func >= (lhs: Demand, rhs: Demand) -> Bool {
@@ -301,6 +362,12 @@ extension Subscribers {
}
}
/// Returns a Boolean that indicates whether the first demand requests more
/// elements than the second.
/// If both sides are `.unlimited`, the result is always `false`.
/// If `lhs` is `.unlimited`, then the result is always `true`.
/// If `rhs` is `.unlimited` then the result is always `false`.
/// Otherwise, this operator compares the demands `max` values.
@inline(__always)
@inlinable
public static func > (lhs: Demand, rhs: Demand) -> Bool {
@@ -316,8 +383,9 @@ extension Subscribers {
}
}
/// Returns `true` if `lhs` and `rhs` are equal. `.unlimited` is not equal to any
/// integer.
/// Returns a Boolean value indicating whether a demand requests the given number
/// of elements.
/// An `.unlimited` demand doesnt match any integer.
@inline(__always)
@inlinable
public static func == (lhs: Demand, rhs: Int) -> Bool {
@@ -328,8 +396,9 @@ extension Subscribers {
}
}
/// Returns `true` if `lhs` and `rhs` are not equal. `.unlimited` is not equal to
/// any integer.
/// Returns a Boolean value indicating whether a demand is not equal to
/// an integer.
/// The `.unlimited` value isnt equal to any integer.
@inlinable
public static func != (lhs: Demand, rhs: Int) -> Bool {
if lhs == .unlimited {
@@ -339,8 +408,9 @@ extension Subscribers {
}
}
/// Returns `true` if `lhs` and `rhs` are equal. `.unlimited` is not equal to any
/// integer.
/// Returns a Boolean value indicating whether a given number of elements matches
/// the request of a given demand.
/// An `.unlimited` demand doesnt match any integer.
@inlinable
public static func == (lhs: Int, rhs: Demand) -> Bool {
if rhs == .unlimited {
@@ -350,8 +420,9 @@ extension Subscribers {
}
}
/// Returns `true` if `lhs` and `rhs` are not equal. `.unlimited` is not equal to
/// any integer.
/// Returns a Boolean value indicating whether an integer is not equal to
/// a demand.
/// The `.unlimited` value isnt equal to any integer.
@inlinable
public static func != (lhs: Int, rhs: Demand) -> Bool {
if rhs == .unlimited {
@@ -366,7 +437,7 @@ extension Subscribers {
return lhs.rawValue == rhs.rawValue
}
/// Returns the number of requested values, or `nil` if `.unlimited`.
/// The number of requested values, or nil if `.unlimited`.
@inlinable public var max: Int? {
if self == .unlimited {
return nil
@@ -344,8 +344,10 @@ extension DispatchQueue {
#if !canImport(Combine)
extension DispatchQueue: OpenCombine.Scheduler {
/// Options that affect the operation of the dispatch queue scheduler.
public typealias SchedulerOptions = OCombine.SchedulerOptions
/// The scheduler time type used by the dispatch queue.
public typealias SchedulerTimeType = OCombine.SchedulerTimeType
public var minimumTolerance: OCombine.SchedulerTimeType.Stride {
@@ -0,0 +1,291 @@
//
// RunLoop.swift
//
//
// Created by Sergej Jaskiewicz on 13.12.2019.
//
import CoreFoundation
import Dispatch
import Foundation
import OpenCombine
extension RunLoop {
/// A namespace for disambiguation when both OpenCombine and Combine are imported.
///
/// Foundation overlay for Combine extends `RunLoop` with new methods and nested
/// types.
/// If you import both OpenCombine and Foundation, you will not be able
/// to write `RunLoop.SchedulerTimeType`,
/// because Swift is unable to understand which `SchedulerTimeType`
/// you're referring to.
///
/// So you have to write `RunLoop.OCombine.SchedulerTimeType`.
///
/// This bug is tracked [here](https://bugs.swift.org/browse/SR-11183).
///
/// You can omit this whenever Combine is not available (e. g. on Linux).
public struct OCombine: Scheduler {
public let runLoop: RunLoop
public init(_ queue: RunLoop) {
self.runLoop = queue
}
/// The scheduler time type used by the run loop.
public struct SchedulerTimeType: Strideable, Codable, Hashable {
/// The date represented by this type.
public var date: Date
/// Initializes a run loop scheduler time with the given date.
///
/// - Parameter date: The date to represent.
public init(_ date: Date) {
self.date = date
}
/// Returns the distance to another run loop scheduler time.
///
/// - Parameter other: Another run loop time.
/// - Returns: The time interval between this time and the provided time.
public func distance(to other: SchedulerTimeType) -> Stride {
let absoluteSelf = date.timeIntervalSinceReferenceDate
let absoluteOther = other.date.timeIntervalSinceReferenceDate
return Stride(absoluteSelf.distance(to: absoluteOther))
}
/// Returns a run loop scheduler time calculated by advancing this instances
/// time by the given interval.
///
/// - Parameter value: A time interval to advance.
/// - Returns: A run loop time advanced by the given interval from this
/// instances time.
public func advanced(by value: Stride) -> SchedulerTimeType {
return SchedulerTimeType(date + value.magnitude)
}
/// The interval by which run loop times advance.
public struct Stride: SchedulerTimeIntervalConvertible,
Comparable,
SignedNumeric,
ExpressibleByFloatLiteral,
Codable {
public typealias FloatLiteralType = TimeInterval
public typealias IntegerLiteralType = TimeInterval
/// A type that can represent the absolute value of any possible value
/// of the conforming type.
public typealias Magnitude = TimeInterval
/// The value of this time interval in seconds.
public var magnitude: TimeInterval
/// The value of this time interval in seconds.
public var timeInterval: TimeInterval { return magnitude }
public init(integerLiteral value: TimeInterval) {
self.magnitude = value
}
public init(floatLiteral value: TimeInterval) {
self.magnitude = value
}
public init(_ timeInterval: TimeInterval) {
self.magnitude = timeInterval
}
public init?<Source: BinaryInteger>(exactly source: Source) {
guard let value = TimeInterval(exactly: source) else { return nil }
magnitude = value
}
public static func < (lhs: Stride, rhs: Stride) -> Bool {
return lhs.magnitude < rhs.magnitude
}
public static func * (lhs: Stride, rhs: Stride) -> Stride {
return Stride(lhs.magnitude * rhs.magnitude)
}
public static func + (lhs: Stride, rhs: Stride) -> Stride {
return Stride(lhs.magnitude + rhs.magnitude)
}
public static func - (lhs: Stride, rhs: Stride) -> Stride {
return Stride(lhs.magnitude - rhs.magnitude)
}
public static func *= (lhs: inout Stride, rhs: Stride) {
lhs.magnitude *= rhs.magnitude
}
public static func += (lhs: inout Stride, rhs: Stride) {
lhs.magnitude += rhs.magnitude
}
public static func -= (lhs: inout Stride, rhs: Stride) {
lhs.magnitude -= rhs.magnitude
}
public static func seconds(_ value: Int) -> Stride {
return Stride(TimeInterval(value))
}
public static func seconds(_ value: Double) -> Stride {
return Stride(TimeInterval(value))
}
public static func milliseconds(_ value: Int) -> Stride {
return Stride(TimeInterval(value) / 1_000)
}
public static func microseconds(_ value: Int) -> Stride {
return Stride(TimeInterval(value) / 1_000_000)
}
public static func nanoseconds(_ value: Int) -> Stride {
return Stride(TimeInterval(value) / 1_000_000_000)
}
}
}
/// Options that affect the operation of the run loop scheduler.
public struct SchedulerOptions {
}
public func schedule(options: SchedulerOptions?, _ action: @escaping () -> Void) {
let cfRunLoop = runLoop.getCFRunLoop()
CFRunLoopPerformBlock(cfRunLoop, defaultRunLoopModeString, action)
CFRunLoopWakeUp(cfRunLoop)
}
public func schedule(after date: SchedulerTimeType,
tolerance: SchedulerTimeType.Stride,
options: SchedulerOptions?,
_ action: @escaping () -> Void) {
let timer = CFRunLoopTimerCreateWithHandler(
nil,
date.date.timeIntervalSinceReferenceDate,
0,
0,
0,
{ _ in action() }
)
// A bug in Combine. The schedule(after:tolerance:options:_:) methods
// always executes the action on the current runloop.
// (FB7493579 if Apple folks are watching)
let theWrongRunLoop = CFRunLoopGetCurrent()
CFRunLoopAddTimer(theWrongRunLoop, timer, defaultRunLoopMode)
CFRunLoopWakeUp(theWrongRunLoop)
}
public func schedule(after date: SchedulerTimeType,
interval: SchedulerTimeType.Stride,
tolerance: SchedulerTimeType.Stride,
options: SchedulerOptions?,
_ action: @escaping () -> Void) -> Cancellable {
let timer = CFRunLoopTimerCreateWithHandler(
nil,
date.date.timeIntervalSinceReferenceDate,
interval.magnitude,
0,
0,
{ _ in action() }
)
let cfRunLoop = runLoop.getCFRunLoop()
CFRunLoopAddTimer(cfRunLoop, timer, defaultRunLoopMode)
CFRunLoopWakeUp(cfRunLoop)
return AnyCancellable { CFRunLoopTimerInvalidate(timer) }
}
public var now: SchedulerTimeType {
return .init(Date())
}
public var minimumTolerance: SchedulerTimeType.Stride {
return .init(0)
}
}
/// A namespace for disambiguation when both OpenCombine and Foundation are imported.
///
/// Foundation overlay for Combine extends `RunLoop` with new methods and nested
/// types.
/// If you import both OpenCombine and Foundation, you will not be able
/// to write `RunLoop.main.schedule { doThings() }`,
/// because Swift is unable to understand which `schedule` method
/// you're referring to.
///
/// So you have to write `RunLoop.main.ocombine.schedule { doThings() }`.
///
/// This bug is tracked [here](https://bugs.swift.org/browse/SR-11183).
///
/// You can omit this whenever Combine is not available (e. g. on Linux).
public var ocombine: OCombine {
return OCombine(self)
}
}
#if !canImport(Combine)
extension RunLoop: OpenCombine.Scheduler {
/// Options that affect the operation of the run loop scheduler.
public typealias SchedulerOptions = OCombine.SchedulerOptions
/// The scheduler time type used by the run loop.
public typealias SchedulerTimeType = OCombine.SchedulerTimeType
public func schedule(options: SchedulerOptions?, _ action: @escaping () -> Void) {
ocombine.schedule(options: options, action)
}
public func schedule(after date: SchedulerTimeType,
tolerance: SchedulerTimeType.Stride,
options: SchedulerOptions?,
_ action: @escaping () -> Void) {
ocombine.schedule(after: date, tolerance: tolerance, options: options, action)
}
public func schedule(after date: SchedulerTimeType,
interval: SchedulerTimeType.Stride,
tolerance: SchedulerTimeType.Stride,
options: SchedulerOptions?,
_ action: @escaping () -> Void) -> Cancellable {
return ocombine.schedule(after: date,
interval: interval,
tolerance: tolerance,
options: options,
action)
}
public var now: SchedulerTimeType {
return ocombine.now
}
public var minimumTolerance: SchedulerTimeType.Stride {
return ocombine.minimumTolerance
}
}
#endif
private var defaultRunLoopMode: CFRunLoopMode {
#if canImport(Darwin)
return CFRunLoopMode.defaultMode
#else
return kCFRunLoopDefaultMode
#endif
}
private var defaultRunLoopModeString: CFString {
#if canImport(Darwin)
return CFRunLoopMode.defaultMode.rawValue
#else
return kCFRunLoopDefaultMode
#endif
}
@@ -0,0 +1,511 @@
//
// RunLoopSchedulerTests.swift
//
//
// Created by Sergej Jaskiewicz on 14.12.2019.
//
import Foundation
import XCTest
#if OPENCOMBINE_COMPATIBILITY_TEST
import Combine
#else
import OpenCombine
import OpenCombineFoundation
#endif
@available(macOS 10.15, iOS 13.0, *)
final class RunLoopSchedulerTests: XCTestCase {
// MARK: - Scheduler.SchedulerTimeType
func testSchedulerTimeTypeDistance() {
let time1 = Scheduler.SchedulerTimeType(Date(timeIntervalSince1970: 10_000))
let time2 = Scheduler.SchedulerTimeType(Date(timeIntervalSince1970: 10_431))
let distantFuture = Scheduler.SchedulerTimeType(.distantFuture)
let notSoDistantFuture = Scheduler.SchedulerTimeType(
Date.distantFuture - 1024
)
XCTAssertEqual(time1.distance(to: time2).timeInterval, 431)
XCTAssertEqual(time2.distance(to: time1).timeInterval, -431)
XCTAssertEqual(time1.distance(to: distantFuture).timeInterval, 64_092_201_200)
XCTAssertEqual(distantFuture.distance(to: time1).timeInterval, -64_092_201_200)
XCTAssertEqual(time2.distance(to: distantFuture).timeInterval, 64_092_200_769)
XCTAssertEqual(distantFuture.distance(to: time2).timeInterval, -64_092_200_769)
XCTAssertEqual(time1.distance(to: notSoDistantFuture).timeInterval,
64_092_200_176)
XCTAssertEqual(notSoDistantFuture.distance(to: time1).timeInterval,
-64_092_200_176)
XCTAssertEqual(time2.distance(to: notSoDistantFuture).timeInterval,
64_092_199_745)
XCTAssertEqual(notSoDistantFuture.distance(to: time2).timeInterval,
-64_092_199_745)
XCTAssertEqual(distantFuture.distance(to: distantFuture).timeInterval,
0)
XCTAssertEqual(notSoDistantFuture.distance(to: notSoDistantFuture).timeInterval,
0)
}
func testSchedulerTimeTypeAdvanced() {
let time =
Scheduler.SchedulerTimeType(Date(timeIntervalSinceReferenceDate: 10_000))
let beginningOfTime =
Scheduler.SchedulerTimeType(Date(timeIntervalSinceReferenceDate: 1))
let stride1 = Scheduler.SchedulerTimeType.Stride.seconds(431)
let stride2 = Scheduler.SchedulerTimeType.Stride.seconds(-220)
XCTAssertEqual(time.advanced(by: stride1),
.init(Date(timeIntervalSinceReferenceDate: 10431)))
XCTAssertEqual(time.advanced(by: stride2),
.init(Date(timeIntervalSinceReferenceDate: 9780)))
#if arch(x86_64) || arch(arm64) || arch(s390x) || arch(powerpc64) || arch(powerpc64le)
// 64-bit platforms
XCTAssertEqual(time.advanced(by: .nanoseconds(.max)).date,
Date(timeIntervalSinceReferenceDate: 9223382036.854776))
XCTAssertEqual(time.advanced(by: .seconds(.max)).date,
Date(timeIntervalSinceReferenceDate: 9.223372036854786E+18))
#elseif arch(i386) || arch(arm)
// 32-bit platforms
XCTAssertEqual(time.advanced(by: .nanoseconds(.max)).date,
Date(timeIntervalSinceReferenceDate: 10002.147483647))
XCTAssertEqual(time.advanced(by: .seconds(.max)).date,
Date(timeIntervalSinceReferenceDate: 2147493647))
#else
#error("This architecture isn't known. Add it to the 32-bit or 64-bit line.")
#endif
XCTAssertEqual(beginningOfTime.advanced(by: .nanoseconds(-1000)).date,
Date(timeIntervalSinceReferenceDate: 0.999999))
XCTAssertEqual(beginningOfTime.advanced(by: .seconds(-1000)).date,
Date(timeIntervalSinceReferenceDate: -999.0))
}
func testSchedulerTimeTypeEquatable() {
let time1 =
Scheduler.SchedulerTimeType(Date(timeIntervalSinceReferenceDate: 10000))
let time2 =
Scheduler.SchedulerTimeType(Date(timeIntervalSinceReferenceDate: 10000))
let time3 =
Scheduler.SchedulerTimeType(Date(timeIntervalSinceReferenceDate: 10001))
XCTAssertEqual(time1, time1)
XCTAssertEqual(time2, time2)
XCTAssertEqual(time3, time3)
XCTAssertEqual(time1, time2)
XCTAssertEqual(time2, time1)
XCTAssertNotEqual(time1, time3)
XCTAssertNotEqual(time3, time1)
}
func testSchedulerTimeTypeCodable() throws {
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let time =
Scheduler.SchedulerTimeType(Date(timeIntervalSinceReferenceDate: 1024.75))
let encodedData = try encoder
.encode(time)
let encodedString = String(decoding: encodedData, as: UTF8.self)
XCTAssertEqual(encodedString,
#"{"date":1024.75}"#)
let decodedTime = try decoder
.decode(Scheduler.SchedulerTimeType.self, from: encodedData)
XCTAssertEqual(decodedTime, time)
}
// MARK: - Scheduler.SchedulerTimeType.Stride
func testStrideToTimeInterval() {
XCTAssertEqual(Stride.seconds(2).timeInterval, 2)
XCTAssertEqual(Stride.seconds(2.2).timeInterval, 2.2)
XCTAssertEqual(Stride.seconds(Double.infinity).timeInterval, .infinity)
XCTAssertEqual(Stride.milliseconds(2).timeInterval, 0.002)
XCTAssertEqual(Stride.microseconds(2).timeInterval, 2E-06)
XCTAssertEqual(Stride.nanoseconds(2).timeInterval, 2E-09)
#if arch(x86_64) || arch(arm64) || arch(s390x) || arch(powerpc64) || arch(powerpc64le)
// 64-bit platforms
XCTAssertEqual(Stride.seconds(Int.max).timeInterval, 9.223372036854776E+18)
XCTAssertEqual(Stride.milliseconds(.max).timeInterval, 9.223372036854776E+15)
XCTAssertEqual(Stride.microseconds(.max).timeInterval, 9223372036854.775)
XCTAssertEqual(Stride.nanoseconds(.max).timeInterval, 9223372036.854776)
#elseif arch(i386) || arch(arm)
// 32-bit platforms
XCTAssertEqual(Stride.seconds(Int.max).timeInterval, 2147483647)
XCTAssertEqual(Stride.milliseconds(.max).timeInterval, 2147483.647)
XCTAssertEqual(Stride.microseconds(.max).timeInterval, 2147.483647)
XCTAssertEqual(Stride.nanoseconds(.max).timeInterval, 2.147483647)
#else
#error("This architecture isn't known. Add it to the 32-bit or 64-bit line.")
#endif
}
func testStrideFromTimeInterval() throws {
XCTAssertEqual(Stride(2).magnitude, 2)
XCTAssertEqual(Stride(2.2).magnitude, 2.2)
XCTAssertEqual(Stride(.infinity).magnitude, .infinity)
XCTAssertEqual(Stride(0.002).magnitude, 0.002)
XCTAssertEqual(Stride(2E-06).magnitude, 2E-06)
XCTAssertEqual(Stride(2E-09).magnitude, 2E-09)
XCTAssertEqual(Stride(9.223372036854776E+18).magnitude, 9.223372036854776E+18)
}
func testStrideFromNumericValue() {
XCTAssertEqual((1.2 as Stride).magnitude, 1.2)
XCTAssertEqual((2 as Stride).magnitude, 2)
XCTAssertNil(Stride(exactly: UInt64.max))
XCTAssertEqual(Stride(exactly: 871 as UInt64)?.magnitude, 871)
}
func testStrideComparable() {
XCTAssertLessThan(Stride.nanoseconds(1), .nanoseconds(2))
XCTAssertGreaterThan(Stride.nanoseconds(-2), .microseconds(-10))
XCTAssertLessThan(Stride.milliseconds(2), .seconds(2))
}
func testStrideMultiplication() {
XCTAssertEqual((Stride.nanoseconds(0) * .nanoseconds(61346)).magnitude, 0)
XCTAssertEqual((Stride.nanoseconds(61346) * .nanoseconds(0)).magnitude, 0)
XCTAssertEqual((Stride.nanoseconds(18) * .nanoseconds(1)).magnitude, 1.8E-17)
XCTAssertEqual((Stride.nanoseconds(18) * .microseconds(1)).magnitude, 1.8E-14)
XCTAssertEqual((Stride.nanoseconds(1) * .nanoseconds(18)).magnitude, 1.8E-17)
XCTAssertEqual((Stride.microseconds(1) * .nanoseconds(18)).magnitude, 1.8E-14)
XCTAssertEqual((Stride.nanoseconds(15) * .nanoseconds(2)).magnitude, 3E-17)
XCTAssertEqual((Stride.microseconds(-3) * .nanoseconds(10)).magnitude, -3E-14)
do {
var stride = Stride.nanoseconds(0)
stride *= .nanoseconds(61346)
XCTAssertEqual(stride.magnitude, 0)
}
do {
var stride = Stride.nanoseconds(61346)
stride *= .nanoseconds(0)
XCTAssertEqual(stride.magnitude, 0)
}
do {
var stride = Stride.nanoseconds(18)
stride *= .nanoseconds(1)
XCTAssertEqual(stride.magnitude, 1.8E-17)
}
do {
var stride = Stride.nanoseconds(18)
stride *= .microseconds(1)
XCTAssertEqual(stride.magnitude, 1.8E-14)
}
do {
var stride = Stride.nanoseconds(1)
stride *= .nanoseconds(18)
XCTAssertEqual(stride.magnitude, 1.8E-17)
}
do {
var stride = Stride.microseconds(1)
stride *= .nanoseconds(18)
XCTAssertEqual(stride.magnitude, 1.8E-14)
}
do {
var stride = Stride.nanoseconds(15)
stride *= .nanoseconds(2)
XCTAssertEqual(stride.magnitude, 3E-17)
}
do {
var stride = Stride.microseconds(-3)
stride *= .nanoseconds(10)
XCTAssertEqual(stride.magnitude, -3E-14)
}
}
func testStrideAddition() {
XCTAssertEqual((Stride.nanoseconds(0) + .microseconds(2)).magnitude, 2E-06)
XCTAssertEqual((Stride.nanoseconds(2) + .microseconds(0)).magnitude, 2E-09)
XCTAssertEqual((Stride.nanoseconds(7) + .nanoseconds(12)).magnitude,
1.8999999999999998E-08)
XCTAssertEqual((Stride.nanoseconds(12) + .nanoseconds(7)).magnitude,
1.8999999999999998E-08)
XCTAssertEqual((Stride.nanoseconds(7) + .nanoseconds(-12)).magnitude, -5E-09)
XCTAssertEqual((Stride.nanoseconds(-12) + .nanoseconds(7)).magnitude, -5E-09)
XCTAssertEqual((Stride.milliseconds(-12) + .seconds(7)).magnitude, 6.988)
XCTAssertEqual((Stride.seconds(-12) + .milliseconds(7)).magnitude, -11.993)
do {
var stride = Stride.nanoseconds(0)
stride += .microseconds(2)
XCTAssertEqual(stride.magnitude, 2E-06)
}
do {
var stride = Stride.nanoseconds(2)
stride += .microseconds(0)
XCTAssertEqual(stride.magnitude, 2E-09)
}
do {
var stride = Stride.nanoseconds(7)
stride += .nanoseconds(12)
XCTAssertEqual(stride.magnitude, 1.8999999999999998E-08)
}
do {
var stride = Stride.nanoseconds(12)
stride += .nanoseconds(7)
XCTAssertEqual(stride.magnitude, 1.8999999999999998E-08)
}
do {
var stride = Stride.nanoseconds(7)
stride += .nanoseconds(-12)
XCTAssertEqual(stride.magnitude, -5E-09)
}
do {
var stride = Stride.nanoseconds(-12)
stride += .nanoseconds(7)
XCTAssertEqual(stride.magnitude, -5E-09)
}
do {
var stride = Stride.seconds(-12)
stride += .milliseconds(7)
XCTAssertEqual(stride.magnitude, -11.993)
}
do {
var stride = Stride.milliseconds(-12)
stride += .seconds(7)
XCTAssertEqual(stride.magnitude, 6.988)
}
}
func testStrideSubtraction() {
XCTAssertEqual((Stride.nanoseconds(0) - .microseconds(2)).magnitude, -2E-06)
XCTAssertEqual((Stride.nanoseconds(2) - .microseconds(0)).magnitude, 2E-09)
XCTAssertEqual((Stride.nanoseconds(7) - .nanoseconds(12)).magnitude, -5E-09)
XCTAssertEqual((Stride.nanoseconds(12) - .nanoseconds(7)).magnitude, 5E-09)
XCTAssertEqual((Stride.nanoseconds(7) - .nanoseconds(-12)).magnitude,
1.8999999999999998E-08)
XCTAssertEqual((Stride.nanoseconds(-12) - .nanoseconds(7)).magnitude,
-1.8999999999999998E-08)
XCTAssertEqual((Stride.seconds(-12) - .milliseconds(7)).magnitude, -12.007)
XCTAssertEqual((Stride.milliseconds(-12) - .seconds(7)).magnitude, -7.012)
do {
var stride = Stride.nanoseconds(0)
stride -= .microseconds(2)
XCTAssertEqual(stride.magnitude, -2E-06)
}
do {
var stride = Stride.nanoseconds(2)
stride -= .microseconds(0)
XCTAssertEqual(stride.magnitude, 2E-09)
}
do {
var stride = Stride.nanoseconds(7)
stride -= .nanoseconds(12)
XCTAssertEqual(stride.magnitude, -5E-09)
}
do {
var stride = Stride.nanoseconds(12)
stride -= .nanoseconds(7)
XCTAssertEqual(stride.magnitude, 5E-09)
}
do {
var stride = Stride.nanoseconds(7)
stride -= .nanoseconds(-12)
XCTAssertEqual(stride.magnitude, 1.8999999999999998E-08)
}
do {
var stride = Stride.nanoseconds(-12)
stride -= .nanoseconds(7)
XCTAssertEqual(stride.magnitude, -1.8999999999999998E-08)
}
do {
var stride = Stride.seconds(-12)
stride -= .milliseconds(7)
XCTAssertEqual(stride.magnitude, -12.007)
}
do {
var stride = Stride.milliseconds(-12)
stride -= .seconds(7)
XCTAssertEqual(stride.magnitude, -7.012)
}
}
func testStrideCodable() throws {
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let stride = Stride.seconds(1024.5)
let encodedData = try encoder.encode(stride)
let encodedString = String(decoding: encodedData, as: UTF8.self)
XCTAssertEqual(encodedString, #"{"magnitude":1024.5}"#)
let decodedStride = try decoder
.decode(Stride.self, from: encodedData)
XCTAssertEqual(decodedStride, stride)
}
// MARK: - Scheduler
func testScheduleActionOnceNow() {
let mainRunLoop = RunLoop.main
let now = Date()
var actualDate = Date.distantPast
executeOnBackgroundThread {
makeScheduler(mainRunLoop).schedule {
XCTAssertTrue(Thread.isMainThread)
actualDate = Date()
RunLoop.current.run(until: Date() + 0.01)
}
}
XCTAssertEqual(actualDate, .distantPast)
mainRunLoop.run(until: Date() + 0.05)
XCTAssertEqual(actualDate.timeIntervalSinceReferenceDate,
now.timeIntervalSinceReferenceDate,
accuracy: 0.1)
}
func testScheduleActionOnceLater() {
let mainRunLoop = RunLoop.main
let now = Date()
var actualDate = Date.distantPast
let desiredDelay: TimeInterval = 0.6
executeOnBackgroundThread {
let scheduler = makeScheduler(mainRunLoop)
scheduler
.schedule(after: scheduler.now.advanced(by: .init(desiredDelay))) {
// This is a bug in Combine! (FB7493579)
// This should be XCTAssertTrue. When they fix it, this test will fail
// and we'll know to fix our implementation.
XCTAssertFalse(Thread.isMainThread)
actualDate = Date()
}
RunLoop.current.run(until: Date() + 1)
}
XCTAssertEqual(
actualDate.timeIntervalSinceReferenceDate -
now.timeIntervalSinceReferenceDate,
desiredDelay,
accuracy: desiredDelay / 3
)
}
func testScheduleRepeating() {
let mainRunLoop = RunLoop.main
let expectation5ticks = expectation(description: "5 ticks")
expectation5ticks.expectedFulfillmentCount = 10
let startDate = Date().timeIntervalSinceReferenceDate
let ticks = Atomic([TimeInterval]())
let desiredDelay: TimeInterval = 0.7
let desiredInterval: TimeInterval = 0.3
let cancellable = executeOnBackgroundThread { () -> Cancellable in
let scheduler = makeScheduler(mainRunLoop)
return scheduler
.schedule(after: scheduler.now.advanced(by: .init(desiredDelay)),
interval: .init(desiredInterval)) {
XCTAssertTrue(Thread.isMainThread)
ticks.do {
$0.append(Date().timeIntervalSinceReferenceDate)
}
expectation5ticks.fulfill()
RunLoop.current.run(until: Date() + 0.001)
}
}
XCTAssertEqual(ticks.value.count, 0)
mainRunLoop.run(until: Date() + 0.001)
XCTAssertEqual(ticks.value.count, 0)
wait(for: [expectation5ticks], timeout: 5)
if ticks.value.isEmpty {
XCTFail("The scheduler doesn't work")
return
}
let actualDelay = ticks.value[0] - startDate
let actualIntervals = zip(ticks.value.dropFirst(), ticks.value.dropLast()).map(-)
let averageInterval = actualIntervals.reduce(0, +) / Double(actualIntervals.count)
XCTAssertEqual(actualDelay,
desiredDelay,
accuracy: desiredDelay / 3,
"""
Actual delay (\(actualDelay)) deviates from desired delay \
(\(desiredDelay)) too much
""")
XCTAssertEqual(averageInterval,
desiredInterval,
accuracy: desiredInterval / 3,
"""
Actual average interval (\(averageInterval)) deviates from \
desired interval (\(desiredInterval)) too much.
Actual intervals: \(actualIntervals)
""")
cancellable.cancel()
let numberOfTicksRightAfterCancellation = ticks.value.count
mainRunLoop.run(until: Date() + 1)
let numberOfTicksOneSecondAfterCancellation = ticks.value.count
XCTAssertEqual(numberOfTicksRightAfterCancellation,
numberOfTicksOneSecondAfterCancellation)
}
}
#if OPENCOMBINE_COMPATIBILITY_TEST || !canImport(Combine)
private typealias Scheduler = RunLoop
private func makeScheduler(_ runLoop: RunLoop) -> RunLoop {
return runLoop
}
#else
private typealias Scheduler = RunLoop.OCombine
private func makeScheduler(_ runLoop: RunLoop) -> RunLoop.OCombine {
return runLoop.ocombine
}
#endif
@available(macOS 10.15, iOS 13.0, *)
private typealias Stride = Scheduler.SchedulerTimeType.Stride
@@ -87,7 +87,7 @@ class CustomPublisherBase<Output, Failure: Error>: Publisher, Cancellable {
typealias CustomConnectablePublisher = CustomConnectablePublisherBase<Int, TestingError>
@available(macOS 10.15, iOS 13.0, *)
final class CustomConnectablePublisherBase<Output: Equatable, Failure: Error>
final class CustomConnectablePublisherBase<Output, Failure: Error>
: CustomPublisherBase<Output, Failure>,
ConnectablePublisher
{
@@ -38,11 +38,18 @@ final class CustomSubscription: Subscription, CustomStringConvertible {
var onRequest: ((Subscribers.Demand) -> Void)?
var onCancel: (() -> Void)?
var onDeinit: (() -> Void)?
init(onRequest: ((Subscribers.Demand) -> Void)? = nil,
onCancel: (() -> Void)? = nil) {
onCancel: (() -> Void)? = nil,
onDeinit: (() -> Void)? = nil) {
self.onRequest = onRequest
self.onCancel = onCancel
self.onDeinit = onDeinit
}
deinit {
onDeinit?()
}
var lastRequested: Subscribers.Demand? {
@@ -0,0 +1,83 @@
//
// ExecuteOnBackgroundThread.swift
//
//
// Created by Sergej Jaskiewicz on 04.02.2020.
//
#if canImport(Darwin)
import Darwin
#elseif canImport(Glibc)
import Glibc
#else
#error("How to do threads on this platform?")
#endif
#if canImport(Darwin)
private typealias ThreadPtr = UnsafeMutablePointer<pthread_t?>
#else
private typealias ThreadPtr = UnsafeMutablePointer<pthread_t>
#endif
func executeOnBackgroundThread<ResultType>(
_ body: () -> ResultType
) -> ResultType {
return withoutActuallyEscaping(body) { body in
// We need this because @convention(c) closures can't capture generic params.
var typeErasedBody: () -> UnsafeMutableRawPointer = {
let resultPtr = UnsafeMutablePointer<ResultType>.allocate(capacity: 1)
resultPtr.initialize(to: body())
return UnsafeMutableRawPointer(resultPtr)
}
return withUnsafeMutablePointer(to: &typeErasedBody) { typeErasedBody in
let _backgroundThread = ThreadPtr.allocate(capacity: 1)
defer { _backgroundThread.deallocate() }
var status: Int32 = 0
// We could use Foundation's Thread, but it doesn't work on Linux for some
// reason.
status = pthread_create(
_backgroundThread,
nil,
{ context in
#if canImport(Darwin)
let context = context
#else
let context = context!
#endif
return context
.assumingMemoryBound(to: (() -> UnsafeMutableRawPointer).self)
.pointee()
},
typeErasedBody
)
guard status == 0 else {
preconditionFailure("Could not create a background thread")
}
#if canImport(Darwin)
guard let backgroundThread = _backgroundThread.pointee else {
preconditionFailure("Could not create a background thread")
}
#else
let backgroundThread = _backgroundThread.pointee
#endif
var _resultPtr: UnsafeMutableRawPointer?
status = pthread_join(backgroundThread, &_resultPtr)
guard status == 0, let resultPtr = _resultPtr else {
preconditionFailure("Could not join threads")
}
defer { resultPtr.deallocate() }
return resultPtr.assumingMemoryBound(to: ResultType.self).move()
}
}
}
@@ -68,7 +68,7 @@ func reduceLikeOperatorMirror(file: StaticString = #file,
("downstream", .contains("TrackingSubscriberBase")),
("result", .anything),
("initial", .anything),
("status", .contains("awaitingSubscription")),
("status", .anything),
file: file,
line: line
)
@@ -83,6 +83,7 @@ internal func testReflection<Output, Failure: Error, Operator: Publisher>(
description expectedDescription: String?,
customMirror customMirrorPredicate: ((Mirror) -> Bool)?,
playgroundDescription: String?,
subscriberIsAlsoSubscription: Bool = true,
_ makeOperator: (CustomConnectablePublisherBase<Output, Failure>) -> Operator
) throws {
let publisher = CustomConnectablePublisherBase<Output, Failure>(subscription: nil)
@@ -124,6 +125,43 @@ internal func testReflection<Output, Failure: Error, Operator: Publisher>(
file: file,
line: line
)
if subscriberIsAlsoSubscription {
publisher.send(subscription: CustomSubscription())
let subscription = try XCTUnwrap(tracking.subscriptions.first?.underlying)
XCTAssertEqual((subscription as? CustomStringConvertible)?.description,
expectedDescription,
file: file,
line: line)
if let customMirrorPredicate = customMirrorPredicate {
let customMirror =
try XCTUnwrap((subscription as? CustomReflectable)?.customMirror,
file: file,
line: line)
XCTAssert(customMirrorPredicate(customMirror),
"customMirror doesn't satisfy the predicate",
file: file,
line: line)
} else {
XCTAssertFalse(subscription is CustomReflectable,
"subscription shouldn't conform to CustomReflectable")
}
XCTAssertFalse(subscription is CustomDebugStringConvertible,
"subscription shouldn't conform to CustomDebugStringConvertible",
file: file,
line: line)
XCTAssertEqual(
((subscription as? CustomPlaygroundDisplayConvertible)?
.playgroundDescription as? String),
playgroundDescription,
file: file,
line: line
)
}
}
@available(macOS 10.15, iOS 13.0, *)
@@ -162,7 +200,7 @@ internal func testSubscriptionReflection<Sut: Publisher>(
}
XCTAssertFalse(subscription is CustomDebugStringConvertible,
"subscriber shouldn't conform to CustomDebugStringConvertible",
"Subscription shouldn't conform to CustomDebugStringConvertible",
file: file,
line: line)
@@ -7,7 +7,9 @@
import XCTest
// FIXME: XCTUnwrap is unavailable in Swift Package Manager yet.
// XCTUnwrap is not available when using Swift Package Manager.
// This has been fixed in Swift 5.2, but we want to maintain compatibility.
#if swift(<5.2)
private struct UnwrappingFailure: Error, LocalizedError {
@@ -52,3 +54,4 @@ public func XCTUnwrap<Result>(_ expression: @autoclosure () throws -> Result?,
throw error
}
}
#endif
@@ -74,6 +74,7 @@ final class AssertNoFailureTests: XCTestCase {
("prefix", "PREFIX")
),
playgroundDescription: "AssertNoFailure",
subscriberIsAlsoSubscription: false,
{ $0.assertNoFailure("PREFIX", file: "SomeFile.swift", line: 1987) }
)
}
@@ -151,6 +151,7 @@ final class AutoconnectTests: XCTestCase {
description: "Autoconnect",
customMirror: customMirrorPredicate,
playgroundDescription: "Autoconnect",
subscriberIsAlsoSubscription: false,
{ $0.autoconnect() })
let subscription = CustomSubscription()
@@ -168,6 +168,7 @@ final class BreakpointTests: XCTestCase {
("upstream", .contains("CustomConnectablePublisherBase"))
),
playgroundDescription: "Breakpoint",
subscriberIsAlsoSubscription: false,
{ $0.breakpointOnError() })
}
}
@@ -303,6 +303,33 @@ final class BufferTests: XCTestCase {
whenFull: .customError(unreachable))
}
func testFailWhileSendingValues() throws {
let helper = OperatorTestHelper(
publisherType: CustomPublisher.self,
initialDemand: nil,
receiveValueDemand: .none,
createSut: { $0.buffer(size: 5, prefetch: .byRequest, whenFull: .dropOldest) }
)
helper.tracking.onValue = { _ in
helper.publisher.send(completion: .failure(.oops))
}
XCTAssertEqual(helper.publisher.send(1), .none)
XCTAssertEqual(helper.publisher.send(2), .none)
XCTAssertEqual(helper.publisher.send(3), .none)
XCTAssertEqual(helper.publisher.send(4), .none)
XCTAssertEqual(helper.publisher.send(5), .none)
try XCTUnwrap(helper.downstreamSubscription).request(.max(3))
XCTAssertEqual(helper.tracking.history, [.subscription("Buffer"),
.value(1),
.completion(.failure(.oops)),
.value(2),
.value(3)])
}
func testBufferByRequestDropNewestLifecycle() {
testBufferLifecycle(prefetch: .byRequest,
whenFull: .dropNewest)
@@ -340,7 +367,7 @@ final class BufferTests: XCTestCase {
description: "Buffer",
customMirror: expectedChildren(
("values", "[]"),
("state", .contains("ready(")),
("state", .anything),
("downstreamDemand", "max(0)"),
("terminal", "nil")
),
@@ -429,7 +456,8 @@ final class BufferTests: XCTestCase {
XCTAssertEqual(helper.tracking.history, [.subscription("Buffer"),
.value(1),
.value(2),
.value(3)])
.value(3),
.completion(.failure(.oops))])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
.cancelled,
.requested(.max(3))])
@@ -466,8 +494,7 @@ final class BufferTests: XCTestCase {
.completion(.failure(.oops))])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
.cancelled,
.requested(.max(3)),
.requested(.max(1))])
.requested(.max(3))])
#if OPENCOMBINE_COMPATIBILITY_TEST
@unknown default:
unreachable()
@@ -595,7 +622,7 @@ final class BufferTests: XCTestCase {
.value(0),
.value(1),
.value(2),
.value(3)])
.completion(.failure(.oops))])
#if OPENCOMBINE_COMPATIBILITY_TEST
@unknown default:
unreachable()
@@ -607,8 +634,7 @@ final class BufferTests: XCTestCase {
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
.requested(.none),
.cancelled,
.requested(.max(3)),
.requested(.max(1))])
.requested(.max(3))])
case (.byRequest, _):
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
.requested(.none),
@@ -618,8 +644,7 @@ final class BufferTests: XCTestCase {
XCTAssertEqual(helper.subscription.history, [.requested(.max(5)),
.requested(.none),
.cancelled,
.requested(.max(6)),
.requested(.max(2))])
.requested(.max(6))])
case (.keepFull, _):
XCTAssertEqual(helper.subscription.history, [.requested(.max(5)),
.requested(.none),
@@ -696,15 +721,27 @@ final class BufferTests: XCTestCase {
helper.publisher.send(completion: completion)
helper.publisher.send(completion: .finished) // Should be ignored
helper.publisher.send(completion: .failure(.oops)) // Should be ignored
helper.publisher.send(completion: .failure(.oops))
XCTAssertEqual(helper.tracking.history, [.subscription("Buffer")])
switch completion {
case .finished:
XCTAssertEqual(helper.tracking.history, [.subscription("Buffer")])
case .failure:
XCTAssertEqual(helper.tracking.history, [.subscription("Buffer"),
.completion(.failure(.oops))])
}
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(helper.publisher.send(1), .none)
XCTAssertEqual(helper.publisher.send(2), .none)
XCTAssertEqual(helper.tracking.history, [.subscription("Buffer")])
switch completion {
case .finished:
XCTAssertEqual(helper.tracking.history, [.subscription("Buffer")])
case .failure:
XCTAssertEqual(helper.tracking.history, [.subscription("Buffer"),
.completion(.failure(.oops))])
}
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
try XCTUnwrap(helper.downstreamSubscription).request(.max(2))
@@ -714,14 +751,14 @@ final class BufferTests: XCTestCase {
XCTAssertEqual(helper.tracking.history, [.subscription("Buffer"),
.value(1),
.value(2)])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
.requested(.max(2))])
case .failure:
XCTAssertEqual(helper.tracking.history, [.subscription("Buffer"),
.completion(completion)])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
}
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
.requested(.max(2))])
try XCTUnwrap(helper.downstreamSubscription).request(.max(1))
switch completion {
@@ -736,8 +773,7 @@ final class BufferTests: XCTestCase {
case .failure:
XCTAssertEqual(helper.tracking.history, [.subscription("Buffer"),
.completion(completion)])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
.requested(.max(2))])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
}
}
@@ -790,7 +826,7 @@ final class BufferTests: XCTestCase {
publisher.send(completion: .failure(.oops))
}
XCTAssertEqual(deinitCounter, 4)
XCTAssertEqual(deinitCounter, 6)
downstreamSubscription?.cancel()
XCTAssertEqual(deinitCounter, 6)
@@ -0,0 +1,622 @@
//
// CatchTests.swift
//
//
// Created by Sergej Jaskiewicz on 25.12.2019.
//
import XCTest
#if OPENCOMBINE_COMPATIBILITY_TEST
import Combine
#else
import OpenCombine
#endif
@available(macOS 10.15, iOS 13.0, *)
final class CatchTests: XCTestCase {
// MARK: - Catch
func testSimpleCatch() {
CatchTests
.testWithSequence(expectedSubscription: "Catch") { upstream, new in
upstream.catch { _ in new }
}
}
func testCatchReceiveSubscriptionTwice() throws {
try testReceiveSubscriptionTwice { $0.catch(Fail.init) }
try testReceiveSubscriptionTwice { publisher in
Fail(outputType: Int.self, failure: TestingError.oops)
.catch { _ in publisher }
}
}
func testCatchCrashesOnUnwantedInput() {
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
initialDemand: nil,
receiveValueDemand: .none,
createSut: { $0.catch { _ in Just(-1) } })
assertCrashes {
_ = helper.publisher.send(42)
}
}
func testCatchPreservesDemand() throws {
try CatchTests.testPreservesDemand(expectedSubscription: "Catch") {
$0.catch($1)
}
}
func testCatchUpstreamFinishes() {
CatchTests.testUpstreamFinishes(expectedSubscription: "Catch") {
$0.catch($1)
}
}
func testCatchRecursion() {
testRecursion(expectedSubscription: "Catch") {
$0.catch($1)
}
}
func testCatchReceiveValueBeforeSubscription() {
testReceiveValueBeforeSubscription(value: 1, expected: .crash) {
$0.catch { _ in Just(13) }
}
}
func testCatchReceiveCompletionBeforeSubscription() {
testReceiveCompletionBeforeSubscription(
inputType: Int.self,
expected: .history([]),
{ $0.catch { _ in Just(13) } }
)
}
func testCatchRequestPendingPost() {
CatchTests.testRequestPendingPost(expectedSubscription: "Catch",
{ $0.catch($1) })
}
func testCatchCancelPendingPost() {
CatchTests.testCancelPendingPost(expectedSubscription: "Catch",
{ $0.catch($1) })
}
func testCatchRequestPost() throws {
try CatchTests.testRequestPost(expectedSubscription: "Catch",
{ $0.catch($1) })
}
func testCatchCancellationBeforeRecovering() throws {
try CatchTests.testCancellationBeforeRecovering(expectedSubscription: "Catch",
{ $0.catch($1) })
}
func testCatchCancellationAfterRecovering() throws {
try CatchTests.testCancellationAfterRecovering(expectedSubscription: "Catch",
{ $0.catch($1) })
}
func testCatchUpstreamFailsTwice() {
testUpstreamFailsTwice(expectedSubscription: "Catch") { $0.catch($1) }
}
func testCatchReflection() throws {
try testReflection(parentInput: Int.self,
parentFailure: TestingError.self,
description: "Catch",
customMirror: expectedChildren(
("downstream", .contains("TrackingSubscriberBase")),
("demand", "max(0)")
),
playgroundDescription: "Catch",
{ $0.catch(Fail.init) })
try testReflection(
parentInput: Int.self,
parentFailure: TestingError.self,
description: "Catch",
customMirror: expectedChildren(
("downstream", .contains("TrackingSubscriberBase")),
("demand", "max(0)")
),
playgroundDescription: "Catch",
{ publisher in
Fail(outputType: Int.self, failure: TestingError.oops)
.catch { _ in publisher }
}
)
}
// MARK: - TryCatch
func testSimpleTryCatch() {
CatchTests
.testWithSequence(expectedSubscription: "TryCatch") { upstream, new in
upstream.tryCatch { _ in new }
}
}
func testTryCatchReceiveSubscriptionTwice() throws {
try testReceiveSubscriptionTwice { $0.tryCatch(Fail.init) }
try testReceiveSubscriptionTwice { publisher in
Fail(outputType: Int.self, failure: TestingError.oops)
.tryCatch { _ in publisher }
}
}
func testTryCatchCrashesOnUnwantedInput() {
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
initialDemand: nil,
receiveValueDemand: .none,
createSut: { $0.tryCatch { _ in Just(-1) } })
assertCrashes {
_ = helper.publisher.send(42)
}
}
func testTryCatchPreservesDemand() throws {
try CatchTests.testPreservesDemand(expectedSubscription: "TryCatch") {
$0.tryCatch($1)
}
}
func testTryCatchUpstreamFinishes() {
CatchTests.testUpstreamFinishes(expectedSubscription: "TryCatch") {
$0.tryCatch($1)
}
}
func testTryCatchHandlerThrows() {
var handledErrors = [TestingError]()
func handler(_ error: TestingError) throws -> Just<Int> {
handledErrors.append(error)
throw "oops2" as TestingError
}
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
initialDemand: .unlimited,
receiveValueDemand: .none,
createSut: { $0.tryCatch(handler) })
XCTAssertEqual(helper.publisher.send(1), .none)
XCTAssertEqual(helper.publisher.send(2), .none)
helper.publisher.send(completion: .failure(.oops))
XCTAssertEqual(helper.tracking.history,
[.subscription("TryCatch"),
.value(1),
.value(2),
.completion(.failure("oops2" as TestingError))])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(handledErrors, [.oops])
XCTAssertEqual(helper.publisher.send(-1), .none)
helper.publisher.send(completion: .failure(.oops))
helper.publisher.send(completion: .finished)
XCTAssertEqual(helper.tracking.history,
[.subscription("TryCatch"),
.value(1),
.value(2),
.completion(.failure("oops2" as TestingError)),
.value(-1)])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(handledErrors, [.oops])
}
func testTryCatchRecursion() {
testRecursion(expectedSubscription: "TryCatch") {
$0.tryCatch($1)
}
}
func testTryCatchReceiveValueBeforeSubscription() {
testReceiveValueBeforeSubscription(value: 1, expected: .crash) {
$0.tryCatch { _ in Just(13) }
}
}
func testTryCatchReceiveCompletionBeforeSubscription() {
testReceiveCompletionBeforeSubscription(
inputType: Int.self,
expected: .history([]),
{ $0.tryCatch { _ in Just(13) } }
)
}
func testTryCatchRequestPendingPost() {
CatchTests.testRequestPendingPost(expectedSubscription: "TryCatch",
{ $0.tryCatch($1) })
}
func testTryCatchCancelPendingPost() {
CatchTests.testCancelPendingPost(expectedSubscription: "TryCatch",
{ $0.tryCatch($1) })
}
func testTryCatchRequestPost() throws {
try CatchTests.testRequestPost(expectedSubscription: "TryCatch",
{ $0.tryCatch($1) })
}
func testTryCatchCancellationBeforeRecovering() throws {
try CatchTests.testCancellationBeforeRecovering(expectedSubscription: "TryCatch",
{ $0.tryCatch($1) })
}
func testTryCatchCancellationAfterRecovering() throws {
try CatchTests.testCancellationAfterRecovering(expectedSubscription: "TryCatch",
{ $0.tryCatch($1) })
}
func testTryCatchUpstreamFailsTwice() {
testUpstreamFailsTwice(expectedSubscription: "TryCatch") { $0.tryCatch($1) }
}
func testTryCatchReflection() throws {
try testReflection(parentInput: Int.self,
parentFailure: TestingError.self,
description: "TryCatch",
customMirror: expectedChildren(
("downstream", .contains("TrackingSubscriberBase")),
("demand", "max(0)")
),
playgroundDescription: "TryCatch",
{ $0.tryCatch(Fail.init) })
try testReflection(
parentInput: Int.self,
parentFailure: TestingError.self,
description: "TryCatch",
customMirror: expectedChildren(
("downstream", .contains("TrackingSubscriberBase")),
("demand", "max(0)")
),
playgroundDescription: "TryCatch",
{ publisher in
Fail<Int, TestingError>(error: .oops).tryCatch { _ in publisher }
}
)
}
// MARK: - Generic tests
private typealias TestSequence = Publishers.Sequence<[Int], Never>
private static func testWithSequence<Operator: Publisher>(
expectedSubscription: StringSubscription,
_ makeCatch: (Publishers.TryMap<TestSequence, Int>, TestSequence) -> Operator
) where Operator.Output == Int {
let throwingSequence = TestSequence(sequence: Array(0 ..< 10))
.tryMap { v -> Int in
if v < 5 {
return v
} else {
throw TestingError.oops
}
}
let `catch` = makeCatch(throwingSequence, [3, 2, 1, 0].publisher)
let tracking = TrackingSubscriberBase<Int, Operator.Failure>(
receiveSubscription: { $0.request(.max(1)) },
receiveValue: { _ in .max(1) }
)
`catch`.subscribe(tracking)
XCTAssertEqual(tracking.history, [.subscription(expectedSubscription),
.value(0),
.value(1),
.value(2),
.value(3),
.value(4),
.value(3),
.value(2),
.value(1),
.value(0),
.completion(.finished)])
}
private static func testPreservesDemand<Operator: Publisher>(
expectedSubscription: StringSubscription,
_ makeCatch:
(CustomPublisher,
@escaping (TestingError) -> CustomPublisherBase<Int, Error>) -> Operator
) throws where Operator.Output == Int {
let errorHandlerSubscription = CustomSubscription()
let errorHandlerPublisher = CustomPublisherBase<Int, Error>(
subscription: errorHandlerSubscription
)
let helper = OperatorTestHelper(
publisherType: CustomPublisher.self,
initialDemand: .max(12),
receiveValueDemand: .max(3),
createSut: { makeCatch($0) { _ in errorHandlerPublisher } }
)
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription)])
XCTAssertEqual(helper.subscription.history, [.requested(.max(12))])
try XCTUnwrap(helper.downstreamSubscription).request(.max(5))
XCTAssertEqual(helper.subscription.history, [.requested(.max(12)),
.requested(.max(5))])
for i in 1 ... 8 {
XCTAssertEqual(helper.publisher.send(i), .max(3))
}
helper.publisher.send(completion: .failure(.oops))
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription),
.value(1),
.value(2),
.value(3),
.value(4),
.value(5),
.value(6),
.value(7),
.value(8)])
XCTAssertEqual(helper.subscription.history, [.requested(.max(12)),
.requested(.max(5))])
XCTAssertEqual(errorHandlerSubscription.history, [.requested(.max(33))])
}
private static func testUpstreamFinishes<Operator: Publisher>(
expectedSubscription: StringSubscription,
_ makeCatch: (CustomPublisher,
@escaping (TestingError) -> Fail<Int, Error>) -> Operator
) where Operator.Output == Int {
var counter = 0
let helper = OperatorTestHelper(
publisherType: CustomPublisher.self,
initialDemand: .max(2),
receiveValueDemand: .none,
createSut: { makeCatch($0) { counter += 1; return Fail(error: $0) } }
)
XCTAssertEqual(helper.publisher.send(1), .none)
XCTAssertEqual(helper.publisher.send(2), .none)
helper.publisher.send(completion: .finished)
helper.publisher.send(completion: .finished)
helper.publisher.send(completion: .failure(.oops))
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription),
.value(1),
.value(2),
.completion(.finished)])
XCTAssertEqual(helper.subscription.history, [.requested(.max(2))])
XCTAssertEqual(counter, 0)
}
private func testRecursion<Operator: Publisher>(
expectedSubscription: StringSubscription,
_ makeCatch: @escaping (CustomPublisher,
@escaping (TestingError) -> Just<Int>) -> Operator
) where Operator.Output == Int {
func createSut(_ publisher: CustomPublisher) -> Operator {
return makeCatch(publisher) { _ in
publisher.send(completion: .failure(.oops))
return Just(13)
}
}
let helper = OperatorTestHelper(
publisherType: CustomPublisher.self,
initialDemand: .unlimited,
receiveValueDemand: .none,
createSut: createSut
)
XCTAssertEqual(helper.publisher.send(1), .none)
XCTAssertEqual(helper.publisher.send(2), .none)
assertCrashes {
helper.publisher.send(completion: .failure(.oops))
}
}
private static func testRequestPendingPost<Operator: Publisher>(
expectedSubscription: StringSubscription,
_ makeCatch: (CustomPublisher,
@escaping (TestingError) -> CustomPublisher) -> Operator
) where Operator.Output == Int {
let handlerSubscription = CustomSubscription()
let handlerPublisher = CustomPublisher(subscription: handlerSubscription)
let helper = OperatorTestHelper(
publisherType: CustomPublisher.self,
initialDemand: .max(3),
receiveValueDemand: .none,
createSut: { makeCatch($0) { _ in handlerPublisher } }
)
handlerPublisher.willSubscribe = { _ in
guard let downstreamSubscription = helper.downstreamSubscription else {
XCTFail("missing downstream subscription")
return
}
downstreamSubscription.request(.max(10))
XCTAssertEqual(handlerSubscription.history, [])
}
XCTAssertEqual(helper.publisher.send(1), .none)
helper.publisher.send(completion: .failure(.oops))
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription),
.value(1)])
XCTAssertEqual(helper.subscription.history, [.requested(.max(3))])
XCTAssertEqual(handlerSubscription.history, [.requested(.max(12))])
}
private static func testCancelPendingPost<Operator: Publisher>(
expectedSubscription: StringSubscription,
_ makeCatch: (CustomPublisher,
@escaping (TestingError) -> CustomPublisher) -> Operator
) where Operator.Output == Int {
let handlerSubscription = CustomSubscription()
let handlerPublisher = CustomPublisher(subscription: handlerSubscription)
let helper = OperatorTestHelper(
publisherType: CustomPublisher.self,
initialDemand: .max(3),
receiveValueDemand: .none,
createSut: { makeCatch($0) { _ in handlerPublisher } }
)
handlerPublisher.willSubscribe = { _ in
guard let downstreamSubscription = helper.downstreamSubscription else {
XCTFail("missing downstream subscription")
return
}
downstreamSubscription.cancel()
XCTAssertEqual(handlerSubscription.history, [])
}
helper.publisher.send(completion: .failure(.oops))
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription)])
XCTAssertEqual(helper.subscription.history, [.requested(.max(3))])
XCTAssertEqual(handlerSubscription.history, [.requested(.max(3))])
handlerPublisher.send(completion: .finished)
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription),
.completion(.finished)])
XCTAssertEqual(handlerSubscription.history, [.requested(.max(3))])
}
private static func testRequestPost<Operator: Publisher>(
expectedSubscription: StringSubscription,
_ makeCatch: (CustomPublisher,
@escaping (TestingError) -> CustomPublisher) -> Operator
) throws where Operator.Output == Int {
let handlerSubscription = CustomSubscription()
let handlerPublisher = CustomPublisher(subscription: handlerSubscription)
let helper = OperatorTestHelper(
publisherType: CustomPublisher.self,
initialDemand: nil,
receiveValueDemand: .none,
createSut: { makeCatch($0) { _ in handlerPublisher } }
)
helper.publisher.send(completion: .failure(.oops))
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription)])
XCTAssertEqual(helper.subscription.history, [])
XCTAssertNotNil(handlerPublisher.subscriber)
try XCTUnwrap(helper.downstreamSubscription).request(.max(12))
XCTAssertEqual(handlerSubscription.history, [.requested(.max(12))])
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription)])
XCTAssertEqual(handlerPublisher.send(100), .none)
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription),
.value(100)])
}
private static func testCancellationBeforeRecovering<Operator: Publisher>(
expectedSubscription: StringSubscription,
_ makeCatch: (CustomPublisher,
@escaping (TestingError) -> CustomPublisher) -> Operator
) throws where Operator.Output == Int {
func handler(_ error: TestingError) -> CustomPublisher {
XCTFail("Should not be called")
return CustomPublisher(subscription: CustomSubscription())
}
let helper = OperatorTestHelper(
publisherType: CustomPublisher.self,
initialDemand: .max(1),
receiveValueDemand: .max(3),
createSut: { makeCatch($0, handler) }
)
let downstreamSubscription = try XCTUnwrap(helper.downstreamSubscription)
downstreamSubscription.cancel()
downstreamSubscription.request(.max(199))
XCTAssertEqual(helper.publisher.send(1), .max(3))
helper.publisher.send(completion: .failure(.oops))
XCTAssertEqual(helper.subscription.history, [.requested(.max(1)), .cancelled])
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription),
.value(1)])
}
private static func testCancellationAfterRecovering<Operator: Publisher>(
expectedSubscription: StringSubscription,
_ makeCatch: (CustomPublisher,
@escaping (TestingError) -> CustomPublisher) -> Operator
) throws where Operator.Output == Int {
let handlerSubscription = CustomSubscription()
let handlerPublisher = CustomPublisher(subscription: handlerSubscription)
let helper = OperatorTestHelper(
publisherType: CustomPublisher.self,
initialDemand: nil,
receiveValueDemand: .max(3),
createSut: { makeCatch($0) { _ in handlerPublisher } }
)
helper.publisher.send(completion: .failure(.oops))
let downstreamSubscription = try XCTUnwrap(helper.downstreamSubscription)
downstreamSubscription.cancel()
XCTAssertEqual(handlerPublisher.send(1), .max(3))
handlerPublisher.send(completion: .finished)
handlerPublisher.send(completion: .failure(.oops))
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription),
.value(1)])
XCTAssertEqual(helper.subscription.history, [])
XCTAssertEqual(handlerSubscription.history, [.cancelled])
let extraSubscription = CustomSubscription()
handlerPublisher.send(subscription: extraSubscription)
XCTAssertEqual(extraSubscription.history, [.cancelled])
}
private func testUpstreamFailsTwice<Operator: Publisher>(
expectedSubscription: StringSubscription,
_ makeCatch: (CustomPublisher,
@escaping (TestingError) -> CustomPublisher) -> Operator
) where Operator.Output == Int {
let handlerSubscription = CustomSubscription()
let handlerPublisher = CustomPublisher(subscription: handlerSubscription)
let helper = OperatorTestHelper(
publisherType: CustomPublisher.self,
initialDemand: nil,
receiveValueDemand: .max(3),
createSut: { makeCatch($0) { _ in handlerPublisher } }
)
helper.publisher.send(completion: .failure(.oops))
assertCrashes {
helper.publisher.send(completion: .failure(.oops))
}
}
}
@@ -217,7 +217,7 @@ final class CollectByCountTests: XCTestCase {
description: "CollectByCount",
customMirror: expectedChildren(
("downstream", .contains("TrackingSubscriberBase")),
("upstreamSubscription", "nil"),
("upstreamSubscription", .anything),
("buffer", "[]"),
("count", "53")
),
@@ -196,7 +196,7 @@ final class ConcatenateTests: XCTestCase {
var didSubscribe = false
publisher.didSubscribe = { _ in
XCTAssertEqual(tracking.history, [])
XCTAssertEqual(tracking.history, [.subscription("Concatenate")])
didSubscribe = true
}
@@ -430,7 +430,7 @@ final class ConcatenateTests: XCTestCase {
description: "Concatenate",
customMirror: expectedChildren(
("downstream", .contains("TrackingSubscriberBase")),
("upstreamSubscription", "nil"),
("upstreamSubscription", .anything),
("suffix", .contains("(sequence: [2.0, 3.0, 5.0, 7.0])")),
("demand", "max(0)")
),
@@ -445,7 +445,7 @@ final class DelayTests: XCTestCase {
}
func testDelayReflection() throws {
/// Delay's Inner doesn't customize its reflection
// Delay's Inner doesn't customize its reflection
try testReflection(parentInput: Int.self,
parentFailure: Error.self,
description: nil,
@@ -167,10 +167,10 @@ final class DropUntilOutputTests: XCTestCase {
let dropUntilOutput = publisher.drop(untilOutputFrom: otherPublisher)
let tracking = TrackingSubscriber(
receiveSubscription: { _ in
XCTAssertNotNil(publisher.subscriber)
XCTAssertNotNil(otherPublisher.subscriber)
XCTAssertNil(publisher.subscriber)
XCTAssertNil(otherPublisher.subscriber)
XCTAssertEqual(subscription.history, [])
XCTAssertEqual(otherSubscription.history, [.requested(.max(1))])
XCTAssertEqual(otherSubscription.history, [])
}
)
@@ -220,15 +220,15 @@ final class DropUntilOutputTests: XCTestCase {
dropUntilOutput.subscribe(tracking)
XCTAssertEqual(tracking.history, [.completion(.finished),
.subscription("DropUntilOutput")])
XCTAssertEqual(tracking.history, [.subscription("DropUntilOutput"),
.completion(.finished)])
let subscription = CustomSubscription()
try XCTUnwrap(publisher.subscriber).receive(subscription: subscription)
XCTAssertEqual(subscription.history, [.requested(.max(14))])
XCTAssertEqual(tracking.history, [.completion(.finished),
.subscription("DropUntilOutput")])
XCTAssertEqual(tracking.history, [.subscription("DropUntilOutput"),
.completion(.finished)])
}
func testReusableOtherSubscriber() throws {
@@ -285,7 +285,7 @@ final class DropUntilOutputTests: XCTestCase {
func testDropUntilOutputOtherReceiveValueBeforeSubscription() {
testReceiveValueBeforeSubscription(
value: 42,
expected: .history([.completion(.finished), .subscription("DropUntilOutput")],
expected: .history([.subscription("DropUntilOutput"), .completion(.finished)],
demand: .none),
{ Empty<Int, Never>().drop(untilOutputFrom: $0) }
)
@@ -294,8 +294,8 @@ final class DropUntilOutputTests: XCTestCase {
func testDropUntilOutputReceiveCompletionBeforeSubscription() {
testReceiveCompletionBeforeSubscription(
inputType: Int.self,
expected: .history([.completion(.finished),
.subscription("DropUntilOutput"),
expected: .history([.subscription("DropUntilOutput"),
.completion(.finished),
.completion(.finished)]),
{ $0.drop(untilOutputFrom: Empty<Int, Never>()) }
)
@@ -304,8 +304,8 @@ final class DropUntilOutputTests: XCTestCase {
func testDropUntilOutputOtherReceiveCompletionBeforeSubscription() {
testReceiveCompletionBeforeSubscription(
inputType: Int.self,
expected: .history([.completion(.finished),
.subscription("DropUntilOutput"),
expected: .history([.subscription("DropUntilOutput"),
.completion(.finished),
.completion(.finished)]),
{ Empty<Int, Never>().drop(untilOutputFrom: $0) }
)
@@ -155,7 +155,7 @@ final class EncodeTests: XCTestCase {
customMirror: expectedChildren(
("downstream", .contains("TrackingSubscriberBase")),
("finished", "false"),
("upstreamSubscription", "nil")
("upstreamSubscription", .anything)
),
playgroundDescription: "Encode",
{ $0.encode(encoder: encoder) })
@@ -275,7 +275,7 @@ final class EncodeTests: XCTestCase {
customMirror: expectedChildren(
("downstream", .contains("TrackingSubscriberBase")),
("finished", "false"),
("upstreamSubscription", "nil")
("upstreamSubscription", .anything)
),
playgroundDescription: "Decode",
{ $0.decode(type: Int.self, decoder: decoder) })
@@ -142,32 +142,43 @@ final class FlatMapTests: XCTestCase {
// Simply making it here shows that there's no dealock
}
func testCancelCancels() {
func testCancelCancels() throws {
let upstreamSubscription = CustomSubscription()
let upstreamPublisher = CustomPublisherBase<Int, Never>(
subscription: upstreamSubscription)
subscription: upstreamSubscription
)
let childSubscription = CustomSubscription()
let childPublisher = CustomPublisherBase<Int, Never>(
subscription: childSubscription)
subscription: childSubscription
)
let flatMap = upstreamPublisher.flatMap { _ in childPublisher }
var downstreamSubscription: Subscription?
let downstreamSubscriber = TrackingSubscriberBase<Int, Never>(receiveSubscription:
{
downstreamSubscription = $0
$0.request(.unlimited)
})
let downstreamSubscriber = TrackingSubscriberBase<Int, Never>(
receiveSubscription: {
downstreamSubscription = $0
$0.request(.max(42))
}
)
upstreamSubscription.onCancel = {
XCTAssertEqual(childSubscription.history, [.requested(.max(1)), .cancelled])
}
childSubscription.onCancel = {
XCTAssertEqual(upstreamSubscription.history, [.requested(.unlimited)])
}
flatMap.subscribe(downstreamSubscriber)
XCTAssertEqual(upstreamPublisher.send(1), .none)
downstreamSubscription?.cancel()
try XCTUnwrap(downstreamSubscription).cancel()
XCTAssertEqual(upstreamSubscription.history.last, .cancelled)
XCTAssertEqual(childSubscription.history.last, .cancelled)
XCTAssertEqual(upstreamSubscription.history, [.requested(.unlimited), .cancelled])
XCTAssertEqual(childSubscription.history, [.requested(.max(1)), .cancelled])
}
func testCancelTwice() throws {
@@ -436,10 +447,7 @@ final class FlatMapTests: XCTestCase {
let childSubscription = CustomSubscription()
let child = CustomPublisher(subscription: childSubscription)
// If Apple changes the implementation to use recursive lock,
// we must make sure no stack overflow occurs here,
// which will also be detected as a crash, which is not what we want.
var recursionDepth = 10
var recursionDepth = 5
helper.subscription.onRequest = { _ in
if recursionDepth <= 0 {
return
@@ -450,9 +458,17 @@ final class FlatMapTests: XCTestCase {
XCTAssertEqual(helper.publisher.send(child), .none)
assertCrashes {
child.send(completion: .finished)
}
child.send(completion: .finished)
XCTAssertEqual(helper.tracking.history, [.subscription("FlatMap")])
XCTAssertEqual(helper.subscription.history, [.requested(.max(1)),
.requested(.max(1)),
.requested(.max(1)),
.requested(.max(1)),
.requested(.max(1)),
.requested(.max(1)),
.requested(.max(1))])
XCTAssertEqual(childSubscription.history, [.requested(.max(1))])
}
func testDownstreamLockReentrance() throws {
@@ -208,7 +208,7 @@ final class IgnoreOutputTests: XCTestCase {
description: "IgnoreOutput",
customMirror: expectedChildren(
("downstream", .contains("TrackingSubscriberBase")),
("status", .contains("awaitingSubscription"))
("status", .anything)
),
playgroundDescription: "IgnoreOutput",
{ $0.ignoreOutput() })
@@ -185,6 +185,7 @@ final class MapErrorTests: XCTestCase {
description: "MapError",
customMirror: childrenIsEmpty,
playgroundDescription: "MapError",
subscriberIsAlsoSubscription: false,
{ $0.mapError { $0 } })
}
@@ -126,6 +126,7 @@ final class MapKeyPathTests: XCTestCase {
("keyPath", .contains("KeyPath"))
),
playgroundDescription: "ValueForKey",
subscriberIsAlsoSubscription: false,
{ $0.map(\.doubled) })
try testReflection(parentInput: Int.self,
@@ -136,6 +137,7 @@ final class MapKeyPathTests: XCTestCase {
("keyPath1", .contains("KeyPath"))
),
playgroundDescription: "ValueForKeys",
subscriberIsAlsoSubscription: false,
{ $0.map(\.doubled, \.tripled) })
try testReflection(parentInput: Int.self,
@@ -147,6 +149,7 @@ final class MapKeyPathTests: XCTestCase {
("keyPath2", .contains("KeyPath"))
),
playgroundDescription: "ValueForKeys",
subscriberIsAlsoSubscription: false,
{ $0.map(\.doubled, \.tripled, \.quadripled) })
}
@@ -221,6 +221,7 @@ final class MapTests: XCTestCase {
description: "Map",
customMirror: childrenIsEmpty,
playgroundDescription: "Map",
subscriberIsAlsoSubscription: false,
{ $0.map { $0 * 2 } })
}
@@ -256,7 +256,7 @@ final class ReceiveOnTests: XCTestCase {
helper.publisher.send(completion: .finished)
}
func testWeakCaptureWhenSchedulingValue() {
func testStrongCaptureWhenSchedulingValue() {
let scheduler = VirtualTimeScheduler()
var value: Int?
var subscriberReleased = false
@@ -276,11 +276,11 @@ final class ReceiveOnTests: XCTestCase {
}
XCTAssertFalse(subscriberReleased)
scheduler.executeScheduledActions()
XCTAssertNil(value)
XCTAssertEqual(value, 42)
XCTAssertTrue(subscriberReleased)
}
func testWeakCaptureWhenSchedulingCompletion() {
func testStrongCaptureWhenSchedulingCompletion() {
let scheduler = VirtualTimeScheduler()
var completion: Subscribers.Completion<TestingError>?
var subscriberReleased = false
@@ -300,7 +300,7 @@ final class ReceiveOnTests: XCTestCase {
}
XCTAssertFalse(subscriberReleased)
scheduler.executeScheduledActions()
XCTAssertNil(completion)
XCTAssertEqual(completion, .finished)
XCTAssertTrue(subscriberReleased)
}
@@ -108,6 +108,7 @@ final class ReplaceNilTests: XCTestCase {
description: "Map",
customMirror: childrenIsEmpty,
playgroundDescription: "Map",
subscriberIsAlsoSubscription: false,
{ $0.replaceNil(with: 0) })
}
}
@@ -152,6 +152,7 @@ final class ScanTests: XCTestCase {
("result", "0")
),
playgroundDescription: "Scan",
subscriberIsAlsoSubscription: false,
{ $0.scan(0, +) })
}
@@ -313,7 +314,7 @@ final class ScanTests: XCTestCase {
description: "TryScan",
customMirror: expectedChildren(
("downstream", .contains("TrackingSubscriber")),
("status", .contains("awaitingSubscription")),
("status", .anything),
("result", "0")
),
playgroundDescription: "TryScan",
@@ -180,6 +180,7 @@ final class SetFailureTypeTests: XCTestCase {
description: "SetFailureType",
customMirror: childrenIsEmpty,
playgroundDescription: "SetFailureType",
subscriberIsAlsoSubscription: false,
{ $0.setFailureType(to: Error.self) })
}
}
@@ -134,15 +134,17 @@ final class ShareTests: XCTestCase {
}
func testShareReceiveValueBeforeSubscription() {
testReceiveValueBeforeSubscription(value: 0,
expected: .crash,
{ $0.share() })
testReceiveValueBeforeSubscription(
value: 0,
expected: .history([.subscription("Multicast")], demand: .none),
{ $0.share() }
)
}
func testShareCompletionBeforeSubscription() {
testReceiveCompletionBeforeSubscription(
inputType: Int.self,
expected: .crash,
expected: .history([.subscription("Multicast")]),
{ $0.share() }
)
}
@@ -0,0 +1,821 @@
//
// SwitchToLatestTests.swift
//
//
// Created by Sergej Jaskiewicz on 07.01.2020.
//
import XCTest
#if OPENCOMBINE_COMPATIBILITY_TEST
import Combine
#else
import OpenCombine
#endif
@available(macOS 10.15, iOS 13.0, *)
final class SwitchToLatestTests: XCTestCase {
var cancellables = [AnyCancellable]()
override func tearDown() {
cancellables = []
super.tearDown()
}
func testSwitchToLatestSequenceWithSink() {
var history = [Int]()
(1 ..< 5)
.publisher
.map {
($0 ..< $0 + 4).publisher
}
.switchToLatest()
.sink {
history.append($0)
}.store(in: &cancellables)
XCTAssertEqual(history, [1, 2, 3, 4, 2, 3, 4, 5, 3, 4, 5, 6, 4, 5, 6, 7])
}
func testRequestOneByOne() {
let tracking = TrackingSubscriberBase<Int, Never>(
receiveSubscription: { $0.request(.max(1)) },
receiveValue: { _ in .max(1) }
)
tracking.store(in: &cancellables)
(1 ..< 5)
.publisher
.map {
($0 ..< $0 + 4).publisher
}
.switchToLatest()
.subscribe(tracking)
XCTAssertEqual(tracking.history, [.subscription("SwitchToLatest"),
.value(1),
.value(2),
.value(3),
.value(4),
.value(2),
.value(3),
.value(4),
.value(5),
.value(3),
.value(4),
.value(5),
.value(6),
.value(4),
.value(5),
.value(6),
.value(7),
.completion(.finished)])
}
func testSendsChildValuesFromLatestOuterPublisher() {
let upstreamPublisher =
PassthroughSubject<PassthroughSubject<Int, TestingError>, TestingError>()
let childPublisher1 = PassthroughSubject<Int, TestingError>()
let childPublisher2 = PassthroughSubject<Int, TestingError>()
let switchToLatest = upstreamPublisher.switchToLatest()
let downstreamSubscriber = TrackingSubscriber(receiveSubscription: {
$0.request(.unlimited)
})
switchToLatest.subscribe(downstreamSubscriber)
upstreamPublisher.send(childPublisher1)
upstreamPublisher.send(childPublisher2)
childPublisher1.send(666)
childPublisher2.send(777)
childPublisher1.send(888)
childPublisher2.send(999)
XCTAssertEqual(downstreamSubscriber.history, [.subscription("SwitchToLatest"),
.value(777),
.value(999)])
}
func testDemand() throws {
let helper = OperatorTestHelper(
publisherType: CustomPublisherBase<CustomPublisher, TestingError>.self,
initialDemand: nil,
receiveValueDemand: .max(5),
createSut: { $0.switchToLatest() }
)
XCTAssertEqual(helper.tracking.history, [.subscription("SwitchToLatest")])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
let subscription1 = CustomSubscription()
let nestedPublisher1 = CustomPublisher(subscription: subscription1)
XCTAssertEqual(helper.publisher.send(nestedPublisher1), .none)
XCTAssertNotNil(nestedPublisher1.subscriber)
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(subscription1.history, [])
try XCTUnwrap(helper.downstreamSubscription).request(.max(1)) // demand == 1
XCTAssertEqual(subscription1.history, [.requested(.max(1))])
nestedPublisher1.send(completion: .finished)
try XCTUnwrap(helper.downstreamSubscription).request(.max(41)) // demand == 42
try XCTUnwrap(helper.downstreamSubscription).request(.max(1)) // demand == 43
XCTAssertEqual(subscription1.history, [.requested(.max(1))])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
let subscription2 = CustomSubscription()
let nestedPublisher2 = CustomPublisher(subscription: subscription2)
XCTAssertEqual(helper.publisher.send(nestedPublisher2), .none)
XCTAssertEqual(subscription1.history, [.requested(.max(1))])
XCTAssertEqual(subscription2.history, [.requested(.max(43))])
XCTAssertEqual(nestedPublisher2.send(1), .max(5)) // demand == 42
XCTAssertEqual(nestedPublisher2.send(2), .max(5)) // demand == 41
XCTAssertEqual(subscription2.history, [.requested(.max(43))])
let subscription3 = CustomSubscription()
let nestedPublisher3 = CustomPublisher(subscription: subscription3)
XCTAssertEqual(helper.publisher.send(nestedPublisher3), .none)
XCTAssertEqual(subscription2.history, [.requested(.max(43)), .cancelled])
XCTAssertEqual(subscription3.history, [.requested(.max(51))])
helper.publisher.send(completion: .finished)
try XCTUnwrap(helper.downstreamSubscription).request(.max(9)) // demand == 50
XCTAssertEqual(subscription3.history, [.requested(.max(51)),
.requested(.max(9))])
}
func testCrashesWhenRequestingZeroFromOuterDemand() {
let helper = OperatorTestHelper(
publisherType: CustomPublisherBase<CustomPublisher, TestingError>.self,
initialDemand: nil,
receiveValueDemand: .max(5),
createSut: { $0.switchToLatest() }
)
assertCrashes {
helper.downstreamSubscription?.request(.none)
}
}
func testCrashesWhenReceivingUnwantedValueFromNestedPublisher() {
let helper = OperatorTestHelper(
publisherType: CustomPublisherBase<CustomPublisher, TestingError>.self,
initialDemand: nil,
receiveValueDemand: .none,
createSut: { $0.switchToLatest() }
)
let nestedSubscription = CustomSubscription()
let nestedPublisher = CustomPublisher(subscription: nestedSubscription)
XCTAssertEqual(helper.publisher.send(nestedPublisher), .none)
XCTAssertEqual(helper.tracking.history, [.subscription("SwitchToLatest")])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
assertCrashes {
_ = nestedPublisher.send(-1)
}
}
func testCancelCancels() throws {
typealias NestedPublisher = CustomPublisherBase<Int, Never>
let upstreamSubscription = CustomSubscription()
let upstreamPublisher = CustomPublisherBase<NestedPublisher, Never>(
subscription: upstreamSubscription
)
let childSubscription = CustomSubscription()
let childPublisher = NestedPublisher(subscription: childSubscription)
let switchToLatest = upstreamPublisher.switchToLatest()
var downstreamSubscription: Subscription?
let tracking = TrackingSubscriberBase<Int, Never>(
receiveSubscription: {
downstreamSubscription = $0
$0.request(.max(42))
}
)
upstreamSubscription.onCancel = {
XCTAssertEqual(childSubscription.history, [.requested(.max(42)),
.cancelled])
}
childSubscription.onCancel = {
XCTAssertEqual(upstreamSubscription.history, [.requested(.unlimited)])
}
switchToLatest.subscribe(tracking)
XCTAssertEqual(upstreamPublisher.send(childPublisher), .none)
try XCTUnwrap(downstreamSubscription).cancel()
try XCTUnwrap(downstreamSubscription).cancel()
XCTAssertEqual(upstreamSubscription.history, [.requested(.unlimited), .cancelled])
XCTAssertEqual(childSubscription.history, [.requested(.max(42)), .cancelled])
}
func testInnerFails() {
let helper = OperatorTestHelper(
publisherType: CustomPublisherBase<CustomPublisher, TestingError>.self,
initialDemand: .max(1),
receiveValueDemand: .max(1),
createSut: { $0.switchToLatest() }
)
let subscription = CustomSubscription()
let nestedPublisher = CustomPublisher(subscription: subscription)
XCTAssertEqual(helper.publisher.send(nestedPublisher), .none)
XCTAssertNotNil(nestedPublisher.subscriber)
XCTAssertEqual(nestedPublisher.send(1), .max(1))
XCTAssertEqual(nestedPublisher.send(2), .max(1))
nestedPublisher.send(completion: .failure(.oops))
nestedPublisher.send(completion: .failure(.oops))
nestedPublisher.send(completion: .finished)
XCTAssertEqual(nestedPublisher.send(-1), .none)
XCTAssertEqual(helper.tracking.history, [.subscription("SwitchToLatest"),
.value(1),
.value(2),
.completion(.failure(.oops))])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited), .cancelled])
XCTAssertEqual(subscription.history, [.requested(.max(1))])
}
func testSwitchToLatestOuterReceiveSubscriptionTwice() throws {
let subscription1 = CustomSubscription()
let publisher =
CustomPublisherBase<CustomPublisher, TestingError>(subscription: nil)
var downstreamSubscription: Subscription?
let tracking = TrackingSubscriber(
receiveSubscription: { downstreamSubscription = $0 }
)
tracking.store(in: &cancellables)
publisher.switchToLatest().subscribe(tracking)
XCTAssertEqual(tracking.history, [.subscription("SwitchToLatest")])
publisher.send(subscription: subscription1)
XCTAssertEqual(tracking.history, [.subscription("SwitchToLatest")])
XCTAssertEqual(subscription1.history, [.requested(.unlimited)])
let subscription2 = CustomSubscription()
publisher.send(subscription: subscription2)
XCTAssertEqual(tracking.history, [.subscription("SwitchToLatest")])
XCTAssertEqual(subscription1.history, [.requested(.unlimited)])
XCTAssertEqual(subscription2.history, [.cancelled])
try XCTUnwrap(downstreamSubscription).cancel()
XCTAssertEqual(subscription1.history, [.requested(.unlimited), .cancelled])
let subscription3 = CustomSubscription()
publisher.send(subscription: subscription3)
XCTAssertEqual(subscription3.history, [.cancelled])
}
func testSwitchToLatestInnerReceiveSubscriptionTwice() throws {
let publisher = CustomPublisherBase<CustomPublisher, TestingError>(
subscription: CustomSubscription()
)
let tracking = TrackingSubscriber()
tracking.store(in: &cancellables)
publisher.switchToLatest().subscribe(tracking)
XCTAssertEqual(tracking.history, [.subscription("SwitchToLatest")])
let subscription1 = CustomSubscription()
let nestedPublisher1 = CustomPublisher(subscription: subscription1)
XCTAssertEqual(publisher.send(nestedPublisher1), .none)
XCTAssertEqual(subscription1.history, [])
let subscription2 = CustomSubscription()
nestedPublisher1.send(subscription: subscription2)
XCTAssertEqual(subscription1.history, [])
XCTAssertEqual(subscription2.history, [.cancelled])
let subscription3 = CustomSubscription()
nestedPublisher1.send(subscription: subscription3)
XCTAssertEqual(subscription3.history, [.cancelled])
let nestedPublisher2 = CustomPublisher(subscription: nil)
var subscription4Destroyed = false
do {
let subscription4 = CustomSubscription(
onDeinit: { subscription4Destroyed = true }
)
XCTAssertEqual(publisher.send(nestedPublisher2), .none)
nestedPublisher2.send(subscription: subscription4)
try XCTUnwrap(nestedPublisher2.subscriber).receive(completion: .finished)
XCTAssertEqual(subscription4.history, [])
}
XCTAssert(subscription4Destroyed)
let subscription5 = CustomSubscription()
nestedPublisher2.send(subscription: subscription5)
XCTAssertEqual(subscription5.history, [])
}
func testSwitchToLatestOuterReceiveValueBeforeSubscription() {
testReceiveValueBeforeSubscription(
value: CustomPublisherBase<Int, Never>(subscription: CustomSubscription()),
expected: .history([.subscription("SwitchToLatest")], demand: .none),
{ $0.switchToLatest() }
)
}
func testSwitchToLatestInnerReceiveValueBeforeSubscription() {
let publisher = CustomPublisherBase<CustomPublisher, TestingError>(
subscription: CustomSubscription()
)
let tracking = TrackingSubscriber(receiveSubscription: { $0.request(.unlimited) },
receiveValue: { _ in .max(42) })
tracking.store(in: &cancellables)
publisher.switchToLatest().subscribe(tracking)
XCTAssertEqual(tracking.history, [.subscription("SwitchToLatest")])
let nestedPublisher = CustomPublisher(subscription: nil)
XCTAssertEqual(publisher.send(nestedPublisher), .none)
XCTAssertEqual(nestedPublisher.send(1), .max(42))
XCTAssertEqual(tracking.history, [.subscription("SwitchToLatest"),
.value(1)])
}
func testSwitchToLatestOuterReceiveCompletionBeforeSubscription() {
testReceiveCompletionBeforeSubscription(
inputType: CustomPublisherBase<Int, Never>.self,
expected: .history([.subscription("SwitchToLatest"),
.completion(.finished)]),
{ $0.switchToLatest() }
)
}
func testSwitchToLatestInnerReceiveCompletionBeforeSubscription() {
let publisher = CustomPublisherBase<CustomPublisher, TestingError>(
subscription: CustomSubscription()
)
let tracking = TrackingSubscriber(receiveSubscription: { $0.request(.unlimited) },
receiveValue: { _ in .max(42) })
tracking.store(in: &cancellables)
publisher.switchToLatest().subscribe(tracking)
XCTAssertEqual(tracking.history, [.subscription("SwitchToLatest")])
let nestedPublisher = CustomPublisher(subscription: nil)
XCTAssertEqual(publisher.send(nestedPublisher), .none)
assertCrashes {
nestedPublisher.send(completion: .finished)
}
}
func testSwitchToLatestOuterReceiveCompletionWhileWaitingForInnerSubscription() {
let completions: [Subscribers.Completion<TestingError>] =
[.finished, .failure(.oops)]
for completion in completions {
let publisher = CustomPublisherBase<CustomPublisher, TestingError>(
subscription: CustomSubscription()
)
let tracking = TrackingSubscriber(
receiveSubscription: { $0.request(.unlimited) },
receiveValue: { _ in .max(42) }
)
tracking.store(in: &cancellables)
publisher.switchToLatest().subscribe(tracking)
XCTAssertEqual(tracking.history, [.subscription("SwitchToLatest")])
let nestedPublisher = CustomPublisher(subscription: nil)
XCTAssertEqual(publisher.send(nestedPublisher), .none)
publisher.send(completion: completion)
switch completion {
case .finished:
XCTAssertEqual(tracking.history, [.subscription("SwitchToLatest")])
case .failure:
XCTAssertEqual(tracking.history, [.subscription("SwitchToLatest"),
.completion(.failure(.oops))])
}
let subscription = CustomSubscription()
nestedPublisher.send(subscription: subscription)
switch completion {
case .finished:
XCTAssertEqual(tracking.history, [.subscription("SwitchToLatest")])
case .failure:
XCTAssertEqual(tracking.history, [.subscription("SwitchToLatest"),
.completion(.failure(.oops))])
}
XCTAssertEqual(subscription.history, [.requested(.unlimited)])
}
}
func testSwitchToLatestInnerReceiveFailureBeforeSubscription() {
let publisher = CustomPublisherBase<CustomPublisher, TestingError>(
subscription: CustomSubscription()
)
let tracking = TrackingSubscriber(receiveSubscription: { $0.request(.unlimited) },
receiveValue: { _ in .max(42) })
tracking.store(in: &cancellables)
publisher.switchToLatest().subscribe(tracking)
XCTAssertEqual(tracking.history, [.subscription("SwitchToLatest")])
let nestedPublisher = CustomPublisher(subscription: nil)
XCTAssertEqual(publisher.send(nestedPublisher), .none)
assertCrashes {
nestedPublisher.send(completion: .failure(.oops))
}
}
func testOuterIgnoresInputAfterCancelling() throws {
let helper = OperatorTestHelper(
publisherType: CustomPublisherBase<CustomPublisher, TestingError>.self,
initialDemand: nil,
receiveValueDemand: .none,
createSut: { $0.switchToLatest() }
)
XCTAssertEqual(helper.tracking.history, [.subscription("SwitchToLatest")])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
try XCTUnwrap(helper.downstreamSubscription).cancel()
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited), .cancelled])
let nestedSubscription = CustomSubscription()
let nestedPublisher = CustomPublisher(subscription: nestedSubscription)
XCTAssertEqual(helper.publisher.send(nestedPublisher), .none)
XCTAssertEqual(nestedSubscription.history, [])
XCTAssertNil(nestedPublisher.subscriber)
}
func testOuterIgnoresInputAfterFinishing() throws {
let helper = OperatorTestHelper(
publisherType: CustomPublisherBase<CustomPublisher, TestingError>.self,
initialDemand: nil,
receiveValueDemand: .none,
createSut: { $0.switchToLatest() }
)
XCTAssertEqual(helper.tracking.history, [.subscription("SwitchToLatest")])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
helper.publisher.send(completion: .finished)
let nestedSubscription = CustomSubscription()
let nestedPublisher = CustomPublisher(subscription: nestedSubscription)
XCTAssertEqual(helper.publisher.send(nestedPublisher), .none)
XCTAssertEqual(nestedSubscription.history, [])
XCTAssertNil(nestedPublisher.subscriber)
}
func testInnerIgnoresInputAfterCancelling() throws {
let helper = OperatorTestHelper(
publisherType: CustomPublisherBase<CustomPublisher, TestingError>.self,
initialDemand: .max(1),
receiveValueDemand: .max(100),
createSut: { $0.switchToLatest() }
)
XCTAssertEqual(helper.tracking.history, [.subscription("SwitchToLatest")])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
let nestedSubscription = CustomSubscription()
let nestedPublisher = CustomPublisher(subscription: nestedSubscription)
XCTAssertEqual(helper.publisher.send(nestedPublisher), .none)
XCTAssertEqual(nestedSubscription.history, [.requested(.max(1))])
XCTAssertNotNil(nestedPublisher.subscriber)
try XCTUnwrap(helper.downstreamSubscription).cancel()
XCTAssertEqual(nestedSubscription.history, [.requested(.max(1)), .cancelled])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited), .cancelled])
XCTAssertEqual(nestedPublisher.send(1), .none)
XCTAssertEqual(helper.tracking.history, [.subscription("SwitchToLatest")])
}
func testInnerReceivesInputAfterOuterFinishes() throws {
let helper = OperatorTestHelper(
publisherType: CustomPublisherBase<CustomPublisher, TestingError>.self,
initialDemand: .max(1),
receiveValueDemand: .max(100),
createSut: { $0.switchToLatest() }
)
XCTAssertEqual(helper.tracking.history, [.subscription("SwitchToLatest")])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
let nestedSubscription = CustomSubscription()
let nestedPublisher = CustomPublisher(subscription: nestedSubscription)
XCTAssertEqual(helper.publisher.send(nestedPublisher), .none)
XCTAssertEqual(nestedSubscription.history, [.requested(.max(1))])
XCTAssertNotNil(nestedPublisher.subscriber)
helper.publisher.send(completion: .finished)
XCTAssertEqual(nestedSubscription.history, [.requested(.max(1))])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(nestedPublisher.send(1), .max(100))
XCTAssertEqual(helper.tracking.history, [.subscription("SwitchToLatest"),
.value(1)])
}
func testOuterIgnoresCompletionAfterCancelling() throws {
let helper = OperatorTestHelper(
publisherType: CustomPublisherBase<CustomPublisher, TestingError>.self,
initialDemand: nil,
receiveValueDemand: .none,
createSut: { $0.switchToLatest() }
)
XCTAssertEqual(helper.tracking.history, [.subscription("SwitchToLatest")])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
try XCTUnwrap(helper.downstreamSubscription).cancel()
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited), .cancelled])
helper.publisher.send(completion: .failure(.oops))
XCTAssertEqual(helper.tracking.history, [.subscription("SwitchToLatest")])
}
func testOuterReceiveCompletionTwice() throws {
let helper = OperatorTestHelper(
publisherType: CustomPublisherBase<CustomPublisher, TestingError>.self,
initialDemand: nil,
receiveValueDemand: .none,
createSut: { $0.switchToLatest() }
)
XCTAssertEqual(helper.tracking.history, [.subscription("SwitchToLatest")])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
helper.publisher.send(completion: .finished)
helper.publisher.send(completion: .failure(.oops))
helper.publisher.send(completion: .finished)
helper.publisher.send(completion: .failure(.oops))
XCTAssertEqual(helper.tracking.history, [.subscription("SwitchToLatest"),
.completion(.finished),
.completion(.failure(.oops)),
.completion(.finished),
.completion(.failure(.oops))])
}
func testInnerIgnoresCompletionAfterCancelling() throws {
let helper = OperatorTestHelper(
publisherType: CustomPublisherBase<CustomPublisher, TestingError>.self,
initialDemand: .max(1),
receiveValueDemand: .max(100),
createSut: { $0.switchToLatest() }
)
XCTAssertEqual(helper.tracking.history, [.subscription("SwitchToLatest")])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
let nestedSubscription = CustomSubscription()
let nestedPublisher = CustomPublisher(subscription: nestedSubscription)
XCTAssertEqual(helper.publisher.send(nestedPublisher), .none)
XCTAssertEqual(nestedSubscription.history, [.requested(.max(1))])
XCTAssertNotNil(nestedPublisher.subscriber)
try XCTUnwrap(helper.downstreamSubscription).cancel()
XCTAssertEqual(nestedSubscription.history, [.requested(.max(1)), .cancelled])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited), .cancelled])
nestedPublisher.send(completion: .failure(.oops))
XCTAssertEqual(helper.tracking.history, [.subscription("SwitchToLatest")])
}
func testInnerIgnoresEventsAfterOuterFails() throws {
let helper = OperatorTestHelper(
publisherType: CustomPublisherBase<CustomPublisher, TestingError>.self,
initialDemand: .max(1),
receiveValueDemand: .max(100),
createSut: { $0.switchToLatest() }
)
XCTAssertEqual(helper.tracking.history, [.subscription("SwitchToLatest")])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
let nestedSubscription = CustomSubscription()
let nestedPublisher = CustomPublisher(subscription: nestedSubscription)
XCTAssertEqual(helper.publisher.send(nestedPublisher), .none)
XCTAssertEqual(nestedSubscription.history, [.requested(.max(1))])
XCTAssertNotNil(nestedPublisher.subscriber)
helper.publisher.send(completion: .failure(.oops))
XCTAssertEqual(nestedSubscription.history, [.requested(.max(1)), .cancelled])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(nestedPublisher.send(1), .max(100))
nestedPublisher.send(completion: .finished)
nestedPublisher.send(completion: .failure(.oops))
XCTAssertEqual(helper.tracking.history, [.subscription("SwitchToLatest"),
.completion(.failure(.oops)),
.value(1)])
}
func testInnerIgnoresEventsAfterOuterFinishes() throws {
let helper = OperatorTestHelper(
publisherType: CustomPublisherBase<CustomPublisher, TestingError>.self,
initialDemand: .max(1),
receiveValueDemand: .max(100),
createSut: { $0.switchToLatest() }
)
XCTAssertEqual(helper.tracking.history, [.subscription("SwitchToLatest")])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
let nestedSubscription = CustomSubscription()
let nestedPublisher = CustomPublisher(subscription: nestedSubscription)
XCTAssertEqual(helper.publisher.send(nestedPublisher), .none)
XCTAssertEqual(nestedSubscription.history, [.requested(.max(1))])
XCTAssertNotNil(nestedPublisher.subscriber)
helper.publisher.send(completion: .finished)
XCTAssertEqual(nestedSubscription.history, [.requested(.max(1))])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(nestedPublisher.send(1), .max(100))
nestedPublisher.send(completion: .finished)
nestedPublisher.send(completion: .failure(.oops))
XCTAssertEqual(helper.tracking.history, [.subscription("SwitchToLatest"),
.value(1),
.completion(.finished)])
}
func testOuterFinishesThenInnerFinishes() {
let helper = OperatorTestHelper(
publisherType: CustomPublisherBase<CustomPublisher, TestingError>.self,
initialDemand: .max(1),
receiveValueDemand: .max(100),
createSut: { $0.switchToLatest() }
)
XCTAssertEqual(helper.tracking.history, [.subscription("SwitchToLatest")])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
let nestedSubscription = CustomSubscription()
let nestedPublisher = CustomPublisher(subscription: nestedSubscription)
XCTAssertEqual(helper.publisher.send(nestedPublisher), .none)
helper.publisher.send(completion: .finished)
XCTAssertEqual(helper.tracking.history, [.subscription("SwitchToLatest")])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(nestedSubscription.history, [.requested(.max(1))])
nestedPublisher.send(completion: .finished)
XCTAssertEqual(helper.tracking.history, [.subscription("SwitchToLatest"),
.completion(.finished)])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(nestedSubscription.history, [.requested(.max(1))])
}
func testInnerFinishesThenOuterFinishes() {
let helper = OperatorTestHelper(
publisherType: CustomPublisherBase<CustomPublisher, TestingError>.self,
initialDemand: .max(1),
receiveValueDemand: .max(100),
createSut: { $0.switchToLatest() }
)
XCTAssertEqual(helper.tracking.history, [.subscription("SwitchToLatest")])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
let nestedSubscription = CustomSubscription()
let nestedPublisher = CustomPublisher(subscription: nestedSubscription)
XCTAssertEqual(helper.publisher.send(nestedPublisher), .none)
nestedPublisher.send(completion: .finished)
XCTAssertEqual(helper.tracking.history, [.subscription("SwitchToLatest")])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(nestedSubscription.history, [.requested(.max(1))])
helper.publisher.send(completion: .finished)
XCTAssertEqual(helper.tracking.history, [.subscription("SwitchToLatest"),
.completion(.finished)])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(nestedSubscription.history, [.requested(.max(1))])
}
func testSwitchToLatestLifecycle() throws {
try testLifecycle(sendValue: CustomPublisher(subscription: CustomSubscription()),
cancellingSubscriptionReleasesSubscriber: false,
{ $0.switchToLatest() })
}
func testSwitchToLatestReflection() throws {
let publisher = CustomPublisherBase<CustomPublisher, TestingError>(
subscription: CustomSubscription()
)
let tracking = TrackingSubscriber()
tracking.store(in: &cancellables)
publisher.switchToLatest().subscribe(tracking)
XCTAssert(publisher.erasedSubscriber is Subscription)
guard let outer = publisher.erasedSubscriber,
let downstreamSubscription = tracking.subscriptions.first?.underlying else {
XCTFail("Missing subscriber/subscription")
return
}
XCTAssert(type(of: outer) == type(of: downstreamSubscription))
let outerCombineID = try XCTUnwrap((outer as? Subscription)?.combineIdentifier)
XCTAssertEqual(downstreamSubscription.combineIdentifier, outerCombineID)
func testReflections(_ subject: Any,
hasChildren: Bool,
file: StaticString = #file,
line: UInt = #line) {
XCTAssertEqual((subject as? CustomStringConvertible)?.description,
"SwitchToLatest",
file: file,
line: line)
XCTAssertFalse(subject is CustomDebugStringConvertible,
file: file,
line: line)
XCTAssertEqual(
(subject as? CustomPlaygroundDisplayConvertible)?
.playgroundDescription as? String,
"SwitchToLatest",
file: file,
line: line
)
if let mirror = (subject as? CustomReflectable)?.customMirror {
if hasChildren {
XCTAssert(expectedChildren(
("parentSubscription",
.matches(String(describing: outerCombineID))),
file: file,
line: line
)(mirror),
file: file,
line: line)
}
} else {
XCTFail("subject should conform to CustomReflectable",
file: file,
line: line)
}
}
testReflections(outer, hasChildren: false)
testReflections(downstreamSubscription, hasChildren: false)
let nestedPublisher = CustomPublisher(subscription: CustomSubscription())
_ = publisher.send(nestedPublisher)
guard let side = nestedPublisher
.erasedSubscriber as? CustomCombineIdentifierConvertible else {
XCTFail("Missing Side")
return
}
XCTAssertFalse(side is Subscription)
XCTAssert(type(of: side) != type(of: outer))
XCTAssert(type(of: side) != type(of: downstreamSubscription))
XCTAssertNotEqual(side.combineIdentifier, outerCombineID)
testReflections(side, hasChildren: true)
}
}
+29
View File
@@ -0,0 +1,29 @@
# To use `opencombine_lldb.py`, figure out its full path.
# Let's say the full path is `~/projects/OpenCombine/opencombine_lldb.py`.
# Then add the following statement to your `~/.lldbinit` file:
#
# command script import ~/projects/OpenCombine/opencombine_lldb.py
import lldb
# Show a Demand as either `max(N)` or `unlimited`.
def summary_Demand(sb_value, internal_dict):
child = sb_value.GetChildAtIndex(0)
if not child.IsValid():
return 'failed to get child of Demand'
number = child.GetValueAsUnsigned()
# .unlimited is represented by a rawValue of UInt(Int.max) + 1.
# Int.max is either 2**31 - 1 or 2**63 - 1 depending on the
# target platform. So .unlimited is either 2**31 or 2**63.
# 31 = 4 * 8 - 1
# 63 = 8 * 8 - 1
unlimited = 2**(child.GetByteSize() * 8 - 1)
if number == unlimited:
return 'unlimited'
else:
return 'max(%d)' % number
def __lldb_init_module(debugger, internal_dict):
debugger.HandleCommand('type summary add -w swift OpenCombine.Subscribers.Demand -F "' + __name__ + '.summary_Demand"')