Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d57c878651 | |||
| 7fa91778c2 | |||
| d15e604764 | |||
| 07c7a98d72 | |||
| 01ef05be1f | |||
| beee9d0d51 | |||
| aacd1a326c | |||
| 5528adcc67 | |||
| 1b810d0536 | |||
| 8b25238154 | |||
| 9b9915bde7 | |||
| 27f01e5f21 | |||
| 739eb47409 | |||
| 14d5a90e89 | |||
| 0e869bc861 | |||
| 2f38069166 | |||
| 97d07d0a14 | |||
| d3888a3808 | |||
| d2b8709afb | |||
| a28177e9c5 | |||
| cef19fce4b | |||
| 7f6bba62de |
@@ -75,3 +75,35 @@ generic_type_name:
|
||||
attributes:
|
||||
always_on_line_above:
|
||||
- "@usableFromInline"
|
||||
|
||||
custom_rules:
|
||||
no_foundation_dependency:
|
||||
included: Sources/OpenCombine
|
||||
name: "No Foundation Dependency"
|
||||
regex: "^import.*Foundation.*$"
|
||||
message: "We don't want to depend on Foundation"
|
||||
severity: error
|
||||
|
||||
no_dispatch_dependency:
|
||||
included: Sources/OpenCombine
|
||||
name: "No Dispatch Dependency"
|
||||
regex: "^import.*Dispatch.*$"
|
||||
message: "We don't want to depend on Dispatch"
|
||||
severity: error
|
||||
|
||||
inheritance_colon:
|
||||
name: "Inheritance Colon"
|
||||
regex: '\s[A-Z_]\w*(<[\w\s:\.,]+>)?(?: +:\s*|:(?:\s{0}|\s{2,}))([\[|\(]*\S)'
|
||||
message: "Colons should be next to the identifier of the inheriting type"
|
||||
severity: warning
|
||||
match_kinds:
|
||||
- identifier
|
||||
- typeidentifier
|
||||
|
||||
dictionary_type_colon:
|
||||
name: "Dictionary Type Colon"
|
||||
regex: '\[\w+(?:(?:\s{0}|\s{2,}):| :(?:\s{0}|\s{2,})\w)'
|
||||
message: "Colon should be surrounded by a single whitespace in a dictionary literal"
|
||||
severity: warning
|
||||
match_kinds:
|
||||
- typeidentifier
|
||||
|
||||
+37
-8
@@ -2,17 +2,27 @@ language: generic
|
||||
|
||||
addons:
|
||||
homebrew:
|
||||
taps:
|
||||
- danger/tap
|
||||
packages:
|
||||
- swiftlint
|
||||
- danger-swift
|
||||
update: true
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- .build
|
||||
- ~/.danger-swift
|
||||
- ~/.swiftenv
|
||||
- ~/Library/Caches/Homebrew
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- name: "Ubuntu 16.04 | Swift 5.1 | Tests"
|
||||
os: linux
|
||||
dist: xenial
|
||||
sudo: required
|
||||
env: SWIFT_VERSION="swift-5.1-DEVELOPMENT-SNAPSHOT-2019-06-16-a" OPENCOMBINE_TEST="YES"
|
||||
env: SWIFT_VERSION="swift-5.1-DEVELOPMENT-SNAPSHOT-2019-09-05-a" OPENCOMBINE_TEST="YES"
|
||||
- name: "macOS 10.14 | Swift 5.0 | Tests"
|
||||
os: osx
|
||||
osx_image: xcode10.2
|
||||
@@ -21,10 +31,14 @@ matrix:
|
||||
# os: osx
|
||||
# osx_image: xcode11
|
||||
# env: SWIFT_VERSION="5.1" OPENCOMBINE_COMPATIBILITY_TEST="YES"
|
||||
- name: "macOS 10.14 | Swift 5.0 | SwiftLint"
|
||||
- name: "macOS 10.14 | Swift 5.0 | Code Quality"
|
||||
os: osx
|
||||
osx_image: xcode10.2
|
||||
env: SWIFT_VERSION="5.0" SWIFT_LINT="YES"
|
||||
env: SWIFT_VERSION="5.0" RUN_DANGER="YES" SWIFT_LINT="YES"
|
||||
|
||||
before_cache:
|
||||
- brew cleanup
|
||||
|
||||
before_install:
|
||||
- if [[ $TRAVIS_OS_NAME == "linux" ]]; then
|
||||
cat /proc/cpuinfo;
|
||||
@@ -38,21 +52,36 @@ install:
|
||||
fi
|
||||
script:
|
||||
- if [[ $OPENCOMBINE_TEST == "YES" ]]; then
|
||||
swift test -c debug --enable-code-coverage --sanitize thread;
|
||||
if [[ $TRAVIS_OS_NAME == "linux" ]]; then
|
||||
make SWIFT_TEST_FLAGS="--enable-test-discovery --enable-index-store" test-debug;
|
||||
else
|
||||
make test-debug;
|
||||
fi
|
||||
fi
|
||||
- if [[ $OPENCOMBINE_TEST == "YES" ]]; then
|
||||
swift test -c release;
|
||||
if [[ $TRAVIS_OS_NAME == "linux" ]]; then
|
||||
make SWIFT_TEST_FLAGS="--enable-test-discovery --enable-index-store" test-release;
|
||||
else
|
||||
make test-release;
|
||||
fi
|
||||
fi
|
||||
- if [[ $OPENCOMBINE_COMPATIBILITY_TEST == "YES" ]]; then
|
||||
swift package generate-xcodeproj --xcconfig-overrides iOS-Combine-Compatibility.xcconfig;
|
||||
set -o pipefail && xcodebuild -scheme OpenCombine-Package -sdk iphonesimulator13.0 -destination "platform=iOS Simulator,name=iPhone Xs,OS=13.0" build test | xcpretty;
|
||||
make generate-compatibility-xcodeproj;
|
||||
set -o pipefail && xcodebuild \
|
||||
-scheme OpenCombine-Package \
|
||||
-sdk iphonesimulator13.0 \
|
||||
-destination "platform=iOS Simulator,name=iPhone Xs,OS=13.0" \
|
||||
build test | xcpretty;
|
||||
fi
|
||||
- if [[ $SWIFT_LINT == "YES" ]]; then
|
||||
swiftlint lint --strict --reporter "emoji";
|
||||
fi
|
||||
- if [[ $RUN_DANGER == "YES" ]]; then
|
||||
danger-swift ci;
|
||||
fi
|
||||
after_success:
|
||||
- if [[ $CODE_COVERAGE == "YES" ]]; then
|
||||
swift package generate-xcodeproj --enable-code-coverage;
|
||||
make generate-xcodeproj;
|
||||
xcodebuild -scheme OpenCombine-Package build test | xcpretty;
|
||||
bash <(curl -s https://codecov.io/bash);
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import Danger
|
||||
|
||||
let danger = Danger()
|
||||
|
||||
SwiftLint.lint(inline: true,
|
||||
configFile: ".swiftlint.yml",
|
||||
strict: true,
|
||||
lintAllFiles: true)
|
||||
|
||||
if danger.warnings.isEmpty, danger.fails.isEmpty {
|
||||
markdown("LGTM")
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
SWIFT_TEST_FLAGS=
|
||||
|
||||
debug:
|
||||
swift build -c debug
|
||||
|
||||
release:
|
||||
swift build -c release
|
||||
|
||||
test-debug:
|
||||
swift test -c debug --sanitize thread $(SWIFT_TEST_FLAGS)
|
||||
|
||||
test-release:
|
||||
swift test -c release $(SWIFT_TEST_FLAGS)
|
||||
|
||||
swift-version:
|
||||
swift -version
|
||||
|
||||
test-compatibility:
|
||||
swift test -Xswiftc -DOPENCOMBINE_COMPATIBILITY_TEST
|
||||
|
||||
generate-compatibility-xcodeproj:
|
||||
swift package generate-xcodeproj --xcconfig-overrides Combine-Compatibility.xcconfig; \
|
||||
open OpenCombine.xcodeproj
|
||||
|
||||
generate-xcodeproj:
|
||||
swift package generate-xcodeproj --enable-code-coverage
|
||||
|
||||
clean:
|
||||
rm -rf .build
|
||||
|
||||
.PHONY: debug release \
|
||||
test-debug \
|
||||
test-release \
|
||||
swift-version \
|
||||
test-compatibility-debug \
|
||||
generate-compatibility-xcodeproj \
|
||||
generate-xcodeproj \
|
||||
clean
|
||||
+2
-1
@@ -13,6 +13,7 @@ let package = Package(
|
||||
targets: [
|
||||
.target(name: "OpenCombine"),
|
||||
.testTarget(name: "OpenCombineTests",
|
||||
dependencies: ["OpenCombine", "GottaGoFast"])
|
||||
dependencies: ["OpenCombine", "GottaGoFast"],
|
||||
swiftSettings: [.unsafeFlags(["-enable-testing"])])
|
||||
]
|
||||
)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
[](https://codecov.io/gh/broadwaylamb/OpenCombine)
|
||||

|
||||

|
||||
[<img src="https://img.shields.io/badge/slack-OpenCombine-yellow.svg?logo=slack">](https://join.slack.com/t/opencombine/shared_invite/enQtNzE2MjE5NzkxODI0LTYxMjkzNDUxZWViZWI1Njc2YjBhODgxNjRjOTdkZTcxOGU2ZjJjZjYxMGI3NWZkN2RkNGFmZTUzNmU3MGE2ZWM)
|
||||
|
||||
Open-source implementation of Apple's [Combine](https://developer.apple.com/documentation/combine) framework for processing values over time.
|
||||
|
||||
|
||||
@@ -115,82 +115,6 @@ extension Publisher {
|
||||
public func measureInterval<S>(using scheduler: S, options: S.SchedulerOptions? = nil) -> Publishers.MeasureInterval<Self, S> where S : Scheduler
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that republishes all elements that match a provided closure.
|
||||
public struct Filter<Upstream> : Publisher where Upstream : Publisher {
|
||||
|
||||
/// 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 = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// A closure that indicates whether to republish an element.
|
||||
public let isIncluded: (Upstream.Output) -> Bool
|
||||
|
||||
public init(upstream: Upstream, isIncluded: @escaping (Output) -> Bool)
|
||||
|
||||
/// 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, Upstream.Failure == S.Failure, Upstream.Output == S.Input
|
||||
}
|
||||
|
||||
/// A publisher that republishes all elements that match a provided error-throwing closure.
|
||||
public struct TryFilter<Upstream> : Publisher where Upstream : Publisher {
|
||||
|
||||
/// 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
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// A error-throwing closure that indicates whether to republish an element.
|
||||
public let isIncluded: (Upstream.Output) throws -> Bool
|
||||
|
||||
public init(upstream: Upstream, isIncluded: @escaping (Upstream.Output) throws -> Bool)
|
||||
|
||||
/// 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, Upstream.Output == S.Input, S.Failure == Publishers.TryFilter<Upstream>.Failure
|
||||
}
|
||||
}
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Republishes all elements that match a provided closure.
|
||||
///
|
||||
/// - Parameter isIncluded: A closure that takes one element and returns a Boolean value indicating whether to republish the element.
|
||||
/// - Returns: A publisher that republishes all elements that satisfy the closure.
|
||||
public func filter(_ isIncluded: @escaping (Self.Output) -> Bool) -> Publishers.Filter<Self>
|
||||
|
||||
/// Republishes all elements that match a provided error-throwing closure.
|
||||
///
|
||||
/// If the `isIncluded` closure throws an error, the publisher fails with that error.
|
||||
///
|
||||
/// - Parameter isIncluded: A closure that takes one element and returns a Boolean value indicating whether to republish the element.
|
||||
/// - Returns: A publisher that republishes all elements that satisfy the closure.
|
||||
public func tryFilter(_ isIncluded: @escaping (Self.Output) throws -> Bool) -> Publishers.TryFilter<Self>
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that raises a debugger signal when a provided closure needs to stop the process in the debugger.
|
||||
@@ -1164,77 +1088,6 @@ extension Publisher {
|
||||
public func tryReduce<T>(_ initialResult: T, _ nextPartialResult: @escaping (T, Self.Output) throws -> T) -> Publishers.TryReduce<Self, T>
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that republishes all non-`nil` results of calling a closure with each received element.
|
||||
public struct CompactMap<Upstream, Output> : Publisher where Upstream : Publisher {
|
||||
|
||||
/// The kind of errors this publisher might publish.
|
||||
///
|
||||
/// Use `Never` if this `Publisher` does not publish errors.
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// A closure that receives values from the upstream publisher and returns optional values.
|
||||
public let transform: (Upstream.Output) -> Output?
|
||||
|
||||
public init(upstream: Upstream, transform: @escaping (Upstream.Output) -> Output?)
|
||||
|
||||
/// 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 Output == S.Input, S : Subscriber, Upstream.Failure == S.Failure
|
||||
}
|
||||
|
||||
/// A publisher that republishes all non-`nil` results of calling an error-throwing closure with each received element.
|
||||
public struct TryCompactMap<Upstream, Output> : Publisher where Upstream : Publisher {
|
||||
|
||||
/// The kind of errors this publisher might publish.
|
||||
///
|
||||
/// Use `Never` if this `Publisher` does not publish errors.
|
||||
public typealias Failure = Error
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// An error-throwing closure that receives values from the upstream publisher and returns optional values.
|
||||
///
|
||||
/// If this closure throws an error, the publisher fails.
|
||||
public let transform: (Upstream.Output) throws -> Output?
|
||||
|
||||
public init(upstream: Upstream, transform: @escaping (Upstream.Output) throws -> Output?)
|
||||
|
||||
/// 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 Output == S.Input, S : Subscriber, S.Failure == Publishers.TryCompactMap<Upstream, Output>.Failure
|
||||
}
|
||||
}
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Calls a closure with each received element and publishes any returned optional that has a value.
|
||||
///
|
||||
/// - Parameter transform: A closure that receives a value and returns an optional value.
|
||||
/// - Returns: A publisher that republishes all non-`nil` results of calling the transform closure.
|
||||
public func compactMap<T>(_ transform: @escaping (Self.Output) -> T?) -> Publishers.CompactMap<Self, T>
|
||||
|
||||
/// Calls an error-throwing closure with each received element and publishes any returned optional that has a value.
|
||||
///
|
||||
/// If the closure throws an error, the publisher cancels the upstream and sends the thrown error to the downstream receiver as a `Failure`.
|
||||
/// - Parameter transform: an error-throwing closure that receives a value and returns an optional value.
|
||||
/// - Returns: A publisher that republishes all non-`nil` results of calling the transform closure.
|
||||
public func tryCompactMap<T>(_ transform: @escaping (Self.Output) throws -> T?) -> Publishers.TryCompactMap<Self, T>
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher created by applying the merge function to two upstream publishers.
|
||||
@@ -1777,43 +1630,6 @@ extension Publisher {
|
||||
public func tryLast(where predicate: @escaping (Self.Output) throws -> Bool) -> Publishers.TryLastWhere<Self>
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that ignores all upstream elements, but passes along a completion state (finish or failed).
|
||||
public struct IgnoreOutput<Upstream> : Publisher where Upstream : Publisher {
|
||||
|
||||
/// The kind of values published by this publisher.
|
||||
public typealias Output = Never
|
||||
|
||||
/// The kind of errors this publisher might publish.
|
||||
///
|
||||
/// Use `Never` if this `Publisher` does not publish errors.
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
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, Upstream.Failure == S.Failure, S.Input == Publishers.IgnoreOutput<Upstream>.Output
|
||||
}
|
||||
}
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Ingores all upstream elements, but passes along a completion state (finished or failed).
|
||||
///
|
||||
/// The output type of this publisher is `Never`.
|
||||
/// - Returns: A publisher that ignores all upstream elements.
|
||||
public func ignoreOutput() -> Publishers.IgnoreOutput<Self>
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that “flattens” nested publishers.
|
||||
@@ -2147,45 +1963,10 @@ extension Publishers {
|
||||
/// once attached it can begin to receive values.
|
||||
public func receive<S>(subscriber: S) where S : Subscriber, Upstream.Failure == S.Failure, Upstream.Output == S.Input
|
||||
}
|
||||
|
||||
/// A publisher that replaces any errors in the stream with a provided element.
|
||||
public struct ReplaceError<Upstream> : Publisher where Upstream : Publisher {
|
||||
|
||||
/// 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 = Never
|
||||
|
||||
/// The element with which to replace errors from the upstream publisher.
|
||||
public let output: Upstream.Output
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
public init(upstream: Upstream, output: Publishers.ReplaceError<Upstream>.Output)
|
||||
|
||||
/// 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, Upstream.Output == S.Input, S.Failure == Publishers.ReplaceError<Upstream>.Failure
|
||||
}
|
||||
}
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Replaces any errors in the stream with the provided element.
|
||||
///
|
||||
/// If the upstream publisher fails with an error, this publisher emits the provided element, then finishes normally.
|
||||
/// - Parameter output: An element to emit when the upstream publisher fails.
|
||||
/// - Returns: A publisher that replaces an error from the upstream publisher with the provided output element.
|
||||
public func replaceError(with output: Self.Output) -> Publishers.ReplaceError<Self>
|
||||
|
||||
/// Replaces an empty stream with the provided element.
|
||||
///
|
||||
/// If the upstream publisher finishes without producing any elements, this publisher emits the provided element, then finishes normally.
|
||||
@@ -2952,51 +2733,6 @@ extension Publisher {
|
||||
public func tryCatch<P>(_ handler: @escaping (Self.Failure) throws -> P) -> Publishers.TryCatch<Self, P> where P : Publisher, Self.Output == P.Output
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
public struct FlatMap<NewPublisher, Upstream> : Publisher where NewPublisher : Publisher, Upstream : Publisher, NewPublisher.Failure == Upstream.Failure {
|
||||
|
||||
/// The kind of values published by this publisher.
|
||||
public typealias Output = NewPublisher.Output
|
||||
|
||||
/// The kind of errors this publisher might publish.
|
||||
///
|
||||
/// Use `Never` if this `Publisher` does not publish errors.
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
public let upstream: Upstream
|
||||
|
||||
public let maxPublishers: Subscribers.Demand
|
||||
|
||||
public let transform: (Upstream.Output) -> NewPublisher
|
||||
|
||||
public init(upstream: Upstream, maxPublishers: Subscribers.Demand, transform: @escaping (Upstream.Output) -> 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, Upstream.Failure == S.Failure
|
||||
}
|
||||
}
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Transforms all elements from an upstream publisher into a new or existing publisher.
|
||||
///
|
||||
/// `flatMap` merges the output from all returned publishers into a single stream of output.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - maxPublishers: The maximum number of publishers produced by this method.
|
||||
/// - transform: A closure that takes an element as a parameter and returns a publisher
|
||||
/// that produces elements of that type.
|
||||
/// - Returns: A publisher that transforms elements from an upstream publisher into
|
||||
/// a publisher of that element’s type.
|
||||
public func flatMap<T, P>(maxPublishers: Subscribers.Demand = .unlimited, _ transform: @escaping (Self.Output) -> P) -> Publishers.FlatMap<P, Self> where T == P.Output, P : Publisher, Self.Failure == P.Failure
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that delays delivery of elements and completion to the downstream receiver.
|
||||
@@ -3089,127 +2825,6 @@ extension Publisher {
|
||||
public func dropFirst(_ count: Int = 1) -> Publishers.Drop<Self>
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that publishes the first element of a stream, then finishes.
|
||||
public struct First<Upstream> : Publisher where Upstream : Publisher {
|
||||
|
||||
/// 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 = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
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, Upstream.Failure == S.Failure, Upstream.Output == S.Input
|
||||
}
|
||||
|
||||
/// A publisher that only publishes the first element of a stream to satisfy a predicate closure.
|
||||
public struct FirstWhere<Upstream> : Publisher where Upstream : Publisher {
|
||||
|
||||
/// 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 = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The closure that determines whether to publish an element.
|
||||
public let predicate: (Upstream.Output) -> Bool
|
||||
|
||||
public init(upstream: Upstream, predicate: @escaping (Publishers.FirstWhere<Upstream>.Output) -> Bool)
|
||||
|
||||
/// 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, Upstream.Failure == S.Failure, Upstream.Output == S.Input
|
||||
}
|
||||
|
||||
/// A publisher that only publishes the first element of a stream to satisfy a throwing predicate closure.
|
||||
public struct TryFirstWhere<Upstream> : Publisher where Upstream : Publisher {
|
||||
|
||||
/// 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
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The error-throwing closure that determines whether to publish an element.
|
||||
public let predicate: (Upstream.Output) throws -> Bool
|
||||
|
||||
public init(upstream: Upstream, predicate: @escaping (Publishers.TryFirstWhere<Upstream>.Output) throws -> Bool)
|
||||
|
||||
/// 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, Upstream.Output == S.Input, S.Failure == Publishers.TryFirstWhere<Upstream>.Failure
|
||||
}
|
||||
}
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Publishes the first element of a stream, then finishes.
|
||||
///
|
||||
/// If this publisher doesn’t receive any elements, it finishes without publishing.
|
||||
/// - Returns: A publisher that only publishes the first element of a stream.
|
||||
public func first() -> Publishers.First<Self>
|
||||
|
||||
/// Publishes the first element of a stream to satisfy a predicate closure, then finishes.
|
||||
///
|
||||
/// The publisher ignores all elements after the first. If this publisher doesn’t receive any elements, it finishes without publishing.
|
||||
/// - Parameter predicate: A closure that takes an element as a parameter and returns a Boolean value that indicates whether to publish the element.
|
||||
/// - Returns: A publisher that only publishes the first element of a stream that satifies the predicate.
|
||||
public func first(where predicate: @escaping (Self.Output) -> Bool) -> Publishers.FirstWhere<Self>
|
||||
|
||||
/// Publishes the first element of a stream to satisfy a throwing predicate closure, then finishes.
|
||||
///
|
||||
/// The publisher ignores all elements after the first. If this publisher doesn’t receive any elements, it finishes without publishing. If the predicate closure throws, the publisher fails with an error.
|
||||
/// - Parameter predicate: A closure that takes an element as a parameter and returns a Boolean value that indicates whether to publish the element.
|
||||
/// - Returns: A publisher that only publishes the first element of a stream that satifies the predicate.
|
||||
public func tryFirst(where predicate: @escaping (Self.Output) throws -> Bool) -> Publishers.TryFirstWhere<Self>
|
||||
}
|
||||
|
||||
extension Publishers.Filter {
|
||||
|
||||
public func filter(_ isIncluded: @escaping (Publishers.Filter<Upstream>.Output) -> Bool) -> Publishers.Filter<Upstream>
|
||||
|
||||
public func tryFilter(_ isIncluded: @escaping (Publishers.Filter<Upstream>.Output) throws -> Bool) -> Publishers.TryFilter<Upstream>
|
||||
}
|
||||
|
||||
extension Publishers.TryFilter {
|
||||
|
||||
public func filter(_ isIncluded: @escaping (Publishers.TryFilter<Upstream>.Output) -> Bool) -> Publishers.TryFilter<Upstream>
|
||||
|
||||
public func tryFilter(_ isIncluded: @escaping (Publishers.TryFilter<Upstream>.Output) throws -> Bool) -> Publishers.TryFilter<Upstream>
|
||||
}
|
||||
|
||||
extension Just {
|
||||
|
||||
public func prepend(_ elements: Output...) -> Publishers.Sequence<[Output], Just<Output>.Failure>
|
||||
@@ -3295,18 +2910,6 @@ extension Publishers.CollectByCount : Equatable where Upstream : Equatable {
|
||||
public static func == (lhs: Publishers.CollectByCount<Upstream>, rhs: Publishers.CollectByCount<Upstream>) -> Bool
|
||||
}
|
||||
|
||||
extension Publishers.CompactMap {
|
||||
|
||||
public func compactMap<T>(_ transform: @escaping (Output) -> T?) -> Publishers.CompactMap<Upstream, T>
|
||||
|
||||
public func map<T>(_ transform: @escaping (Output) -> T) -> Publishers.CompactMap<Upstream, T>
|
||||
}
|
||||
|
||||
extension Publishers.TryCompactMap {
|
||||
|
||||
public func compactMap<T>(_ transform: @escaping (Output) throws -> T?) -> Publishers.TryCompactMap<Upstream, T>
|
||||
}
|
||||
|
||||
extension Publishers.Merge : Equatable where A : Equatable, B : Equatable {
|
||||
|
||||
/// Returns a Boolean value that indicates whether two publishers are equivalent.
|
||||
@@ -3410,17 +3013,6 @@ extension Publishers.Count : Equatable where Upstream : Equatable {
|
||||
public static func == (lhs: Publishers.Count<Upstream>, rhs: Publishers.Count<Upstream>) -> Bool
|
||||
}
|
||||
|
||||
extension Publishers.IgnoreOutput : Equatable where Upstream : Equatable {
|
||||
|
||||
/// Returns a Boolean value that indicates whether two publishers are equivalent.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - lhs: An ignore output publisher to compare for equality.
|
||||
/// - rhs: Another ignore output publisher to compare for equality.
|
||||
/// - Returns: `true` if the two publishers have equal upstream publishers, `false` otherwise.
|
||||
public static func == (lhs: Publishers.IgnoreOutput<Upstream>, rhs: Publishers.IgnoreOutput<Upstream>) -> Bool
|
||||
}
|
||||
|
||||
extension Publishers.Retry : Equatable where Upstream : Equatable {
|
||||
|
||||
/// Returns a Boolean value indicating whether two values are equal.
|
||||
@@ -3575,40 +3167,6 @@ extension Publishers.First : Equatable where Upstream : Equatable {
|
||||
public static func == (lhs: Publishers.First<Upstream>, rhs: Publishers.First<Upstream>) -> Bool
|
||||
}
|
||||
|
||||
/// Adds a `Publisher` to a property.
|
||||
///
|
||||
/// Properties annotated with `@Published` contain both the stored value and a publisher which sends any new values after the property value has been sent. New subscribers will receive the current value of the property first.
|
||||
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
|
||||
@propertyWrapper public struct Published<Value> {
|
||||
|
||||
/// Initialize the storage of the Published property as well as the corresponding `Publisher`.
|
||||
public init(initialValue: Value)
|
||||
|
||||
public static subscript<EnclosingSelf: AnyObject>(
|
||||
_enclosingInstance object: EnclosingSelf,
|
||||
wrapped wrappedKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Value>,
|
||||
storage storageKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Published<Value>>
|
||||
) -> Value { get set }
|
||||
|
||||
public class Publisher : Publisher {
|
||||
|
||||
public typealias Output = Value
|
||||
|
||||
public typealias Failure = Never
|
||||
|
||||
/// 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 Value == S.Input, S : Subscriber, S.Failure == Published<Value>.Publisher.Failure
|
||||
}
|
||||
|
||||
/// The property that can be accessed with the `$` syntax and allows access to the `Publisher`
|
||||
public var projectedValue: Published<Value>.Publisher { mutating get }
|
||||
}
|
||||
|
||||
/// A type of object with a publisher that emits before the object has changed.
|
||||
///
|
||||
/// By default an `ObservableObject` will synthesize an `objectWillChange`
|
||||
|
||||
@@ -249,6 +249,7 @@ internal final class SubjectSubscriber<Downstream: Subject>
|
||||
|
||||
internal func receive(completion: Subscribers.Completion<Downstream.Failure>) {
|
||||
downstreamSubject?.send(completion: completion)
|
||||
downstreamSubject = nil
|
||||
}
|
||||
|
||||
internal var description: String { return "Subject" }
|
||||
|
||||
@@ -14,6 +14,8 @@ public final class CurrentValueSubject<Output, Failure: Error>: Subject {
|
||||
// TODO: Combine uses bag data structure
|
||||
private var _subscriptions: [Conduit] = []
|
||||
|
||||
private var _value: Output
|
||||
|
||||
private var _completion: Subscribers.Completion<Failure>?
|
||||
|
||||
internal var upstreamSubscriptions: [Subscription] = []
|
||||
@@ -22,8 +24,11 @@ public final class CurrentValueSubject<Output, Failure: Error>: Subject {
|
||||
|
||||
/// The value wrapped by this subject, published as a new element whenever it changes.
|
||||
public var value: Output {
|
||||
didSet {
|
||||
send(value)
|
||||
get {
|
||||
return _value
|
||||
}
|
||||
set {
|
||||
send(newValue)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +36,7 @@ public final class CurrentValueSubject<Output, Failure: Error>: Subject {
|
||||
///
|
||||
/// - Parameter value: The initial value to publish.
|
||||
public init(_ value: Output) {
|
||||
self.value = value
|
||||
self._value = value
|
||||
}
|
||||
|
||||
deinit {
|
||||
@@ -68,6 +73,7 @@ public final class CurrentValueSubject<Output, Failure: Error>: Subject {
|
||||
|
||||
public func send(_ input: Output) {
|
||||
_lock.do {
|
||||
_value = input
|
||||
for subscription in _subscriptions where !subscription.isCompleted {
|
||||
if subscription._demand > 0 {
|
||||
subscription._offer(input)
|
||||
|
||||
@@ -139,9 +139,7 @@ public struct ImmediateScheduler: Scheduler {
|
||||
tolerance: SchedulerTimeType.Stride,
|
||||
options: SchedulerOptions?,
|
||||
_ action: @escaping () -> Void) {
|
||||
fatalError(
|
||||
"Attempt to schedule something in the future on the immediate scheduler"
|
||||
)
|
||||
action()
|
||||
}
|
||||
|
||||
/// Performs the action at some time after the specified date, at the specified
|
||||
@@ -151,8 +149,7 @@ public struct ImmediateScheduler: Scheduler {
|
||||
tolerance: SchedulerTimeType.Stride,
|
||||
options: SchedulerOptions?,
|
||||
_ action: @escaping () -> Void) -> Cancellable {
|
||||
fatalError(
|
||||
"Attempt to schedule something in the future on the immediate scheduler"
|
||||
)
|
||||
action()
|
||||
return Subscriptions.empty
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
//
|
||||
// Published.swift
|
||||
// OpenCombine
|
||||
//
|
||||
// Created by Евгений Богомолов on 01/09/2019.
|
||||
//
|
||||
|
||||
#if swift(>=5.1)
|
||||
/// Adds a `Publisher` to a property.
|
||||
///
|
||||
/// Properties annotated with `@Published` contain both the stored value
|
||||
/// and a publisher which sends any new values after the property value
|
||||
/// has been sent. New subscribers will receive the current value
|
||||
/// of the property first.
|
||||
@propertyWrapper public struct Published<Value> {
|
||||
|
||||
/// Initialize the storage of the Published
|
||||
/// property as well as the corresponding `Publisher`.
|
||||
public init(initialValue: Value) {
|
||||
value = initialValue
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
public init(wrappedValue: Value) {
|
||||
value = wrappedValue
|
||||
}
|
||||
|
||||
public struct Publisher: OpenCombine.Publisher {
|
||||
|
||||
/// The kind of values published by this publisher.
|
||||
public typealias Output = Value
|
||||
|
||||
/// The kind of errors this publisher might publish.
|
||||
///
|
||||
/// Use `Never` if this `Publisher` does not publish errors.
|
||||
public typealias Failure = Never
|
||||
|
||||
/// 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<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Input == Value, Downstream.Failure == Never
|
||||
{
|
||||
subject.subscribe(subscriber)
|
||||
}
|
||||
|
||||
fileprivate let subject: OpenCombine.CurrentValueSubject<Value, Never>
|
||||
|
||||
fileprivate init(_ output: Output) {
|
||||
subject = .init(output)
|
||||
}
|
||||
}
|
||||
|
||||
private var value: Value
|
||||
|
||||
/// The property that can be accessed with the
|
||||
/// `$` syntax and allows access to the `Publisher`
|
||||
public var projectedValue: Publisher {
|
||||
mutating get {
|
||||
if let publisher = publisher {
|
||||
return publisher
|
||||
}
|
||||
let publisher = Publisher(value)
|
||||
self.publisher = publisher
|
||||
return publisher
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable, message:
|
||||
"@Published is only available on properties of classes")
|
||||
|
||||
public var wrappedValue: Value {
|
||||
get { value }
|
||||
set {
|
||||
value = newValue
|
||||
publisher?.subject.value = newValue
|
||||
}
|
||||
}
|
||||
|
||||
private var publisher: Publisher?
|
||||
|
||||
@available(*, unavailable, message:
|
||||
"This subscript is unavailable in OpenCombine yet")
|
||||
public static subscript<EnclosingSelf: AnyObject>(
|
||||
_enclosingInstance object: EnclosingSelf,
|
||||
wrapped wrappedKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Value>,
|
||||
storage storageKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Published<Value>>
|
||||
) -> Value {
|
||||
get { fatalError() }
|
||||
set { fatalError() }
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -138,7 +138,7 @@ extension Optional.OCombine.Publisher {
|
||||
}
|
||||
|
||||
public func collect() -> Optional<[Output]>.OCombine.Publisher {
|
||||
return .init(self.output.map { [$0] })
|
||||
return .init(self.output.map { [$0] } ?? [])
|
||||
}
|
||||
|
||||
public func compactMap<ElementOfResult>(
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
//
|
||||
// Publishers.CompactMap.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 11.07.2019.
|
||||
//
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that republishes all non-`nil` results of calling a closure
|
||||
/// with each received element.
|
||||
public struct CompactMap<Upstream: Publisher, Output>: Publisher {
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// A closure that receives values from the upstream publisher
|
||||
/// and returns optional values.
|
||||
public let transform: (Upstream.Output) -> Output?
|
||||
|
||||
public init(upstream: Upstream,
|
||||
transform: @escaping (Upstream.Output) -> Output?) {
|
||||
self.upstream = upstream
|
||||
self.transform = transform
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Input == Output, Downstream.Failure == Failure
|
||||
{
|
||||
let inner = Inner(downstream: subscriber, transform: catching(transform))
|
||||
upstream.subscribe(inner)
|
||||
}
|
||||
}
|
||||
|
||||
/// A publisher that republishes all non-`nil` results of calling an error-throwing
|
||||
/// closure with each received element.
|
||||
public struct TryCompactMap<Upstream: Publisher, Output>: Publisher {
|
||||
|
||||
public typealias Failure = Error
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// An error-throwing closure that receives values from the upstream publisher
|
||||
/// and returns optional values.
|
||||
///
|
||||
/// If this closure throws an error, the publisher fails.
|
||||
public let transform: (Upstream.Output) throws -> Output?
|
||||
|
||||
public init(upstream: Upstream,
|
||||
transform: @escaping (Upstream.Output) throws -> Output?) {
|
||||
self.upstream = upstream
|
||||
self.transform = transform
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Input == Output, Downstream.Failure == Failure
|
||||
{
|
||||
let inner = Inner(downstream: subscriber, transform: catching(transform))
|
||||
upstream.subscribe(inner)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Calls a closure with each received element and publishes any returned
|
||||
/// optional that has a value.
|
||||
///
|
||||
/// - Parameter transform: A closure that receives a value and returns
|
||||
/// an optional value.
|
||||
/// - Returns: A publisher that republishes all non-`nil` results of calling
|
||||
/// the transform closure.
|
||||
public func compactMap<ElementOfResult>(
|
||||
_ transform: @escaping (Output) -> ElementOfResult?
|
||||
) -> Publishers.CompactMap<Self, ElementOfResult> {
|
||||
return .init(upstream: self, transform: transform)
|
||||
}
|
||||
|
||||
/// Calls an error-throwing closure with each received element and publishes
|
||||
/// any returned optional that has a value.
|
||||
///
|
||||
/// If the closure throws an error, the publisher cancels the upstream and sends
|
||||
/// the thrown error to the downstream receiver as a `Failure`.
|
||||
///
|
||||
/// - Parameter transform: an error-throwing closure that receives a value
|
||||
/// and returns an optional value.
|
||||
/// - Returns: A publisher that republishes all non-`nil` results of calling
|
||||
/// the `transform` closure.
|
||||
public func tryCompactMap<ElementOfResult>(
|
||||
_ transform: @escaping (Output) throws -> ElementOfResult?
|
||||
) -> Publishers.TryCompactMap<Self, ElementOfResult> {
|
||||
return .init(upstream: self, transform: transform)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.CompactMap {
|
||||
|
||||
public func compactMap<ElementOfResult>(
|
||||
_ transform: @escaping (Output) -> ElementOfResult?
|
||||
) -> Publishers.CompactMap<Upstream, ElementOfResult> {
|
||||
return .init(upstream: upstream,
|
||||
transform: { self.transform($0).flatMap(transform) })
|
||||
}
|
||||
|
||||
public func map<ElementOfResult>(
|
||||
_ transform: @escaping (Output) -> ElementOfResult
|
||||
) -> Publishers.CompactMap<Upstream, ElementOfResult> {
|
||||
return .init(upstream: upstream,
|
||||
transform: { self.transform($0).map(transform) })
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.TryCompactMap {
|
||||
|
||||
public func compactMap<ElementOfResult>(
|
||||
_ transform: @escaping (Output) throws -> ElementOfResult?
|
||||
) -> Publishers.TryCompactMap<Upstream, ElementOfResult> {
|
||||
return .init(upstream: upstream,
|
||||
transform: { try self.transform($0).flatMap(transform) })
|
||||
}
|
||||
}
|
||||
|
||||
private class _CompactMap<Upstream: Publisher, Downstream: Subscriber>
|
||||
: OperatorSubscription<Downstream>,
|
||||
Subscription
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
typealias Failure = Upstream.Failure
|
||||
typealias Transform = (Input) -> Result<Downstream.Input?, Downstream.Failure>
|
||||
|
||||
fileprivate var _transform: Transform?
|
||||
|
||||
var _isCompleted: Bool {
|
||||
return _transform == nil
|
||||
}
|
||||
|
||||
init(downstream: Downstream, transform: @escaping Transform) {
|
||||
_transform = transform
|
||||
super.init(downstream: downstream)
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
upstreamSubscription = subscription
|
||||
downstream.receive(subscription: self)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
guard let transform = _transform else { return .none }
|
||||
|
||||
switch transform(input) {
|
||||
case .success(let output?):
|
||||
return downstream.receive(output)
|
||||
case .success(nil):
|
||||
return .max(1)
|
||||
case .failure(let error):
|
||||
downstream.receive(completion: .failure(error))
|
||||
_transform = nil
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
guard !_isCompleted else { return }
|
||||
upstreamSubscription?.request(demand)
|
||||
}
|
||||
|
||||
override func cancel() {
|
||||
_transform = nil
|
||||
upstreamSubscription?.cancel()
|
||||
upstreamSubscription = nil
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.CompactMap {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: _CompactMap<Upstream, Downstream>,
|
||||
Subscriber,
|
||||
CustomStringConvertible
|
||||
where Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
var description: String { return "CompactMap" }
|
||||
|
||||
func receive(completion: Subscribers.Completion<Upstream.Failure>) {
|
||||
if !_isCompleted {
|
||||
_transform = nil
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.TryCompactMap {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: _CompactMap<Upstream, Downstream>,
|
||||
Subscriber,
|
||||
CustomStringConvertible
|
||||
where Downstream.Failure == Error
|
||||
{
|
||||
var description: String { return "TryCompactMap" }
|
||||
|
||||
func receive(completion: Subscribers.Completion<Upstream.Failure>) {
|
||||
if !_isCompleted {
|
||||
_transform = nil
|
||||
downstream.receive(completion: completion.eraseError())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -127,6 +127,7 @@ private class _DropWhile<Upstream: Publisher, Downstream: Subscriber>
|
||||
override func cancel() {
|
||||
upstreamSubscription?.cancel()
|
||||
upstreamSubscription = nil
|
||||
isCompleted = true
|
||||
// Don't zero out downstream, that's what Combine does (probably a bug)
|
||||
}
|
||||
}
|
||||
@@ -142,11 +143,9 @@ extension Publishers.DropWhile {
|
||||
var description: String { return "DropWhile" }
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
guard !isCompleted else {
|
||||
assertionFailure("unreachable")
|
||||
return
|
||||
}
|
||||
guard !isCompleted else { return }
|
||||
downstream.receive(completion: completion)
|
||||
isCompleted = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -164,6 +163,7 @@ extension Publishers.TryDropWhile {
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
guard !isCompleted else { return }
|
||||
downstream.receive(completion: completion.eraseError())
|
||||
isCompleted = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
//
|
||||
// Publishers.Filter.swift
|
||||
//
|
||||
//
|
||||
// Created by Joseph Spadafora on 7/3/19.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Republishes all elements that match a provided closure.
|
||||
///
|
||||
/// - Parameter isIncluded: A closure that takes one element and returns
|
||||
/// a Boolean value indicating whether to republish the element.
|
||||
/// - Returns: A publisher that republishes all elements that satisfy the closure.
|
||||
public func filter(
|
||||
_ isIncluded: @escaping (Output) -> Bool
|
||||
) -> Publishers.Filter<Self> {
|
||||
return Publishers.Filter(upstream: self, isIncluded: isIncluded)
|
||||
}
|
||||
|
||||
/// Republishes all elements that match a provided error-throwing closure.
|
||||
///
|
||||
/// If the `isIncluded` closure throws an error, the publisher fails with that error.
|
||||
///
|
||||
/// - Parameter isIncluded: A closure that takes one element and returns a
|
||||
/// Boolean value indicating whether to republish the element.
|
||||
/// - Returns: A publisher that republishes all elements that satisfy the closure.
|
||||
public func tryFilter(
|
||||
_ isIncluded: @escaping (Output) throws -> Bool
|
||||
) -> Publishers.TryFilter<Self> {
|
||||
return Publishers.TryFilter(upstream: self, isIncluded: isIncluded)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.Filter {
|
||||
|
||||
public func filter(
|
||||
_ isIncluded: @escaping (Output) -> Bool
|
||||
) -> Publishers.Filter<Upstream> {
|
||||
return .init(upstream: upstream) { self.isIncluded($0) && isIncluded($0) }
|
||||
}
|
||||
|
||||
public func tryFilter(
|
||||
_ isIncluded: @escaping (Output) throws -> Bool
|
||||
) -> Publishers.TryFilter<Upstream> {
|
||||
return .init(upstream: upstream) { try self.isIncluded($0) && isIncluded($0) }
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.TryFilter {
|
||||
|
||||
public func filter(
|
||||
_ isIncluded: @escaping (Output) -> Bool
|
||||
) -> Publishers.TryFilter<Upstream> {
|
||||
return .init(upstream: upstream) { try self.isIncluded($0) && isIncluded($0) }
|
||||
}
|
||||
|
||||
public func tryFilter(
|
||||
_ isIncluded: @escaping (Output) throws -> Bool
|
||||
) -> Publishers.TryFilter<Upstream> {
|
||||
return .init(upstream: upstream) { try self.isIncluded($0) && isIncluded($0) }
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that republishes all elements that match a provided closure.
|
||||
public struct Filter<Upstream: Publisher>: Publisher {
|
||||
|
||||
/// 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 = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// A closure that indicates whether to republish an element.
|
||||
public let isIncluded: (Upstream.Output) -> Bool
|
||||
|
||||
public init(upstream: Upstream, isIncluded: @escaping (Output) -> Bool) {
|
||||
self.upstream = upstream
|
||||
self.isIncluded = isIncluded
|
||||
}
|
||||
|
||||
/// 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<SubscriberType: Subscriber>(subscriber: SubscriberType)
|
||||
where Upstream.Failure == SubscriberType.Failure,
|
||||
Upstream.Output == SubscriberType.Input
|
||||
{
|
||||
let filter = Inner(downstream: subscriber, isIncluded: catching(isIncluded))
|
||||
upstream.receive(subscriber: filter)
|
||||
}
|
||||
}
|
||||
|
||||
/// A publisher that republishes all elements that match
|
||||
/// a provided error-throwing closure.
|
||||
public struct TryFilter<Upstream>: Publisher where Upstream: Publisher {
|
||||
|
||||
/// 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
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// A error-throwing closure that indicates whether to republish an element.
|
||||
public let isIncluded: (Upstream.Output) throws -> Bool
|
||||
|
||||
public init(upstream: Upstream,
|
||||
isIncluded: @escaping (Upstream.Output) throws -> Bool) {
|
||||
self.upstream = upstream
|
||||
self.isIncluded = isIncluded
|
||||
}
|
||||
|
||||
/// 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<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Output == Downstream.Input,
|
||||
Downstream.Failure == Failure
|
||||
{
|
||||
let filter = Inner(downstream: subscriber, isIncluded: catching(isIncluded))
|
||||
upstream.receive(subscriber: filter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class _Filter<Upstream: Publisher, Downstream: Subscriber>
|
||||
: OperatorSubscription<Downstream>,
|
||||
Subscription
|
||||
where Upstream.Output == Downstream.Input
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
typealias Failure = Upstream.Failure
|
||||
typealias Predicate = (Input) -> Result<Bool, Downstream.Failure>
|
||||
|
||||
private var _isIncluded: Predicate?
|
||||
|
||||
var isFinished: Bool {
|
||||
return _isIncluded == nil
|
||||
}
|
||||
|
||||
init(downstream: Downstream, isIncluded: @escaping Predicate) {
|
||||
_isIncluded = isIncluded
|
||||
super.init(downstream: downstream)
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
upstreamSubscription = subscription
|
||||
downstream.receive(subscription: self)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
guard let isIncluded = _isIncluded else { return .none }
|
||||
switch isIncluded(input) {
|
||||
case .success(let isIncluded):
|
||||
return isIncluded ? downstream.receive(input) : .max(1)
|
||||
case .failure(let error):
|
||||
downstream.receive(completion: .failure(error))
|
||||
cancel()
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
guard !isFinished else { return }
|
||||
upstreamSubscription?.request(demand)
|
||||
}
|
||||
|
||||
override func cancel() {
|
||||
_isIncluded = nil
|
||||
upstreamSubscription?.cancel()
|
||||
upstreamSubscription = nil
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.Filter {
|
||||
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: _Filter<Upstream, Downstream>,
|
||||
Subscriber,
|
||||
CustomStringConvertible
|
||||
where Upstream.Output == Downstream.Input,
|
||||
Upstream.Failure == Downstream.Failure {
|
||||
|
||||
var description: String { return "Filter" }
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
guard !isFinished else { return }
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.TryFilter {
|
||||
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: _Filter<Upstream, Downstream>,
|
||||
Subscriber,
|
||||
CustomStringConvertible
|
||||
where Upstream.Output == Downstream.Input, Downstream.Failure == Error {
|
||||
|
||||
var description: String { return "TryFilter" }
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
guard !isFinished else { return }
|
||||
downstream.receive(completion: completion.eraseError())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
//
|
||||
// Publishers.First.swift
|
||||
//
|
||||
//
|
||||
// Created by Joseph Spadafora on 7/8/19.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Publishes the first element of a stream, then finishes.
|
||||
///
|
||||
/// If this publisher doesn’t receive any elements, it finishes without publishing.
|
||||
/// - Returns: A publisher that only publishes the first element of a stream.
|
||||
public func first() -> Publishers.First<Self> {
|
||||
return .init(upstream: self)
|
||||
}
|
||||
|
||||
/// Publishes the first element of a stream to
|
||||
/// satisfy a predicate closure, then finishes.
|
||||
///
|
||||
/// The publisher ignores all elements after the first.
|
||||
/// If this publisher doesn’t receive any elements,
|
||||
/// it finishes without publishing.
|
||||
/// - Parameter predicate: A closure that takes an element as a parameter and
|
||||
/// returns a Boolean value that indicates whether to publish the element.
|
||||
/// - Returns: A publisher that only publishes the first element of a stream
|
||||
/// that satifies the predicate.
|
||||
public func first(
|
||||
where predicate: @escaping (Output) -> Bool
|
||||
) -> Publishers.FirstWhere<Self> {
|
||||
return .init(upstream: self, predicate: predicate)
|
||||
}
|
||||
|
||||
/// Publishes the first element of a stream to satisfy a
|
||||
/// throwing predicate closure, then finishes.
|
||||
///
|
||||
/// The publisher ignores all elements after the first. If this publisher
|
||||
/// doesn’t receive any elements, it finishes without publishing. If the
|
||||
/// predicate closure throws, the publisher fails with an error.
|
||||
/// - Parameter predicate: A closure that takes an element as a parameter and
|
||||
/// returns a Boolean value that indicates whether to publish the element.
|
||||
/// - Returns: A publisher that only publishes the first element of a stream
|
||||
/// that satifies the predicate.
|
||||
public func tryFirst(
|
||||
where predicate: @escaping (Output) throws -> Bool
|
||||
) -> Publishers.TryFirstWhere<Self> {
|
||||
return .init(upstream: self, predicate: predicate)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that publishes the first element of a stream, then finishes.
|
||||
public struct First<Upstream: Publisher>: Publisher {
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
public init(upstream: Upstream) {
|
||||
self.upstream = upstream
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Failure == Downstream.Failure,
|
||||
Output == Downstream.Input
|
||||
{
|
||||
let inner = Inner(downstream: subscriber, predicate: { _ in .success(true) })
|
||||
upstream.receive(subscriber: inner)
|
||||
}
|
||||
}
|
||||
|
||||
/// A publisher that only publishes the first element of a
|
||||
/// stream to satisfy a predicate closure.
|
||||
public struct FirstWhere<Upstream: Publisher>: Publisher {
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The closure that determines whether to publish an element.
|
||||
public let predicate: (Output) -> Bool
|
||||
|
||||
public init(upstream: Upstream, predicate: @escaping (Output) -> Bool) {
|
||||
self.upstream = upstream
|
||||
self.predicate = predicate
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Failure == Downstream.Failure,
|
||||
Output == Downstream.Input
|
||||
{
|
||||
let inner = Inner(downstream: subscriber, predicate: catching(predicate))
|
||||
upstream.receive(subscriber: inner)
|
||||
}
|
||||
}
|
||||
|
||||
/// A publisher that only publishes the first element of a stream
|
||||
/// to satisfy a throwing predicate closure.
|
||||
public struct TryFirstWhere<Upstream: Publisher>: Publisher {
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Error
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The error-throwing closure that determines whether to publish an element.
|
||||
public let predicate: (Output) throws -> Bool
|
||||
|
||||
public init(upstream: Upstream, predicate: @escaping (Output) throws -> Bool) {
|
||||
self.upstream = upstream
|
||||
self.predicate = predicate
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Failure == Downstream.Failure,
|
||||
Output == Downstream.Input
|
||||
{
|
||||
let inner = Inner(downstream: subscriber, predicate: catching(predicate))
|
||||
upstream.receive(subscriber: inner)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class _FirstWhere<Upstream: Publisher, Downstream: Subscriber>
|
||||
: OperatorSubscription<Downstream>,
|
||||
Subscription
|
||||
where Downstream.Input == Upstream.Output
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
typealias Failure = Upstream.Failure
|
||||
typealias Predicate = (Input) -> Result<Bool, Downstream.Failure>
|
||||
|
||||
// ┌──────────────────┐
|
||||
// ┌──────▶│ .pending(input) │───────────┐
|
||||
// │ └──────────────────┘ │
|
||||
// receive(input)│ │request(demand)
|
||||
// │ │
|
||||
// │ │
|
||||
// │ ▼
|
||||
// ┌──────────────────┐ ┌──────────────┐
|
||||
// ●───▶│.waitingForDemand │ │ .finished │
|
||||
// └──────────────────┘ └──────────────┘
|
||||
// │ ▲
|
||||
// │ │
|
||||
// │ │
|
||||
// request(demand)│ │receive(input)
|
||||
// │ ┌──────────────────────────┐ │
|
||||
// └───▶│ .downstreamHasRequested │──────┘
|
||||
// └──────────────────────────┘
|
||||
enum State {
|
||||
case waitingForDemand
|
||||
case pending(Input)
|
||||
case downstreamHasRequested
|
||||
case finished
|
||||
}
|
||||
|
||||
var predicate: Predicate?
|
||||
private var _state: State = .waitingForDemand
|
||||
|
||||
var isCompleted: Bool {
|
||||
return predicate == nil
|
||||
}
|
||||
|
||||
init(downstream: Downstream, predicate: @escaping Predicate) {
|
||||
self.predicate = predicate
|
||||
super.init(downstream: downstream)
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
upstreamSubscription = subscription
|
||||
subscription.request(.unlimited)
|
||||
downstream.receive(subscription: self)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
switch _state {
|
||||
case .pending, .finished:
|
||||
break
|
||||
case .downstreamHasRequested:
|
||||
_ifSatisfiesPredicate(input) {
|
||||
_state = .finished
|
||||
_sendDownstream(input)
|
||||
}
|
||||
case .waitingForDemand:
|
||||
_ifSatisfiesPredicate(input) {
|
||||
_state = .pending(input)
|
||||
}
|
||||
}
|
||||
return .none
|
||||
}
|
||||
|
||||
private func _ifSatisfiesPredicate(_ input: Input, _ onSuccess: () -> Void) {
|
||||
guard let predicate = self.predicate else { return }
|
||||
switch predicate(input) {
|
||||
case .success(true):
|
||||
onSuccess()
|
||||
case .success(false):
|
||||
return
|
||||
case .failure(let error):
|
||||
cancel()
|
||||
downstream.receive(completion: .failure(error))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
private func _sendDownstream(_ input: Input) {
|
||||
_ = downstream.receive(input)
|
||||
cancel()
|
||||
downstream.receive(completion: .finished)
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
precondition(demand > 0, "demand must not be zero")
|
||||
switch _state {
|
||||
case .waitingForDemand:
|
||||
_state = .downstreamHasRequested
|
||||
case .pending(let input):
|
||||
_state = .finished
|
||||
_sendDownstream(input)
|
||||
case .finished, .downstreamHasRequested:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
override func cancel() {
|
||||
predicate = nil
|
||||
upstreamSubscription?.cancel()
|
||||
upstreamSubscription = nil
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.First {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: _FirstWhere<Upstream, Downstream>,
|
||||
Subscriber,
|
||||
CustomStringConvertible
|
||||
where Upstream.Output == Downstream.Input,
|
||||
Upstream.Failure == Downstream.Failure
|
||||
{
|
||||
var description: String { return "First" }
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
guard !isCompleted else { return }
|
||||
predicate = nil
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.FirstWhere {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: _FirstWhere<Upstream, Downstream>,
|
||||
Subscriber,
|
||||
CustomStringConvertible
|
||||
where Upstream.Output == Downstream.Input,
|
||||
Upstream.Failure == Downstream.Failure
|
||||
{
|
||||
var description: String { return "TryFirst" }
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
guard !isCompleted else { return }
|
||||
predicate = nil
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.TryFirstWhere {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: _FirstWhere<Upstream, Downstream>,
|
||||
Subscriber,
|
||||
CustomStringConvertible
|
||||
where Upstream.Output == Downstream.Input,
|
||||
Downstream.Failure == Error
|
||||
{
|
||||
var description: String { return "TryFirstWhere" }
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
guard !isCompleted else { return }
|
||||
predicate = nil
|
||||
downstream.receive(completion: completion.eraseError())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,404 @@
|
||||
//
|
||||
// Publishers.FlatMap.swift
|
||||
//
|
||||
// Created by Eric Patey on 16.08.2019.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Transforms all elements from an upstream publisher into a new or existing
|
||||
/// publisher.
|
||||
///
|
||||
/// `flatMap` merges the output from all returned publishers into a single stream of
|
||||
/// output.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - maxPublishers: The maximum number of publishers produced by this method.
|
||||
/// - transform: A closure that takes an element as a parameter and returns a
|
||||
/// publisher that produces elements of that type.
|
||||
/// - Returns: A publisher that transforms elements from an upstream publisher into
|
||||
/// a publisher of that element’s type.
|
||||
public func flatMap<Result, Child>(maxPublishers: Subscribers.Demand = .unlimited,
|
||||
_ transform: @escaping (Self.Output) -> Child)
|
||||
-> Publishers.FlatMap<Child, Self>
|
||||
where Result == Child.Output, Child: Publisher, Self.Failure == Child.Failure {
|
||||
return Publishers.FlatMap(upstream: self,
|
||||
maxPublishers: maxPublishers,
|
||||
transform: transform)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
public struct FlatMap<Child, Upstream>: Publisher
|
||||
where Child: Publisher, Upstream: Publisher, Child.Failure == Upstream.Failure {
|
||||
|
||||
/// The kind of values published by this publisher.
|
||||
public typealias Output = Child.Output
|
||||
|
||||
/// The kind of errors this publisher might publish.
|
||||
///
|
||||
/// Use `Never` if this `Publisher` does not publish errors.
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
public let upstream: Upstream
|
||||
|
||||
public let maxPublishers: Subscribers.Demand
|
||||
|
||||
public let transform: (Upstream.Output) -> Child
|
||||
|
||||
public init(upstream: Upstream, maxPublishers: Subscribers.Demand,
|
||||
transform: @escaping (Upstream.Output) -> Child) {
|
||||
self.upstream = upstream
|
||||
self.maxPublishers = maxPublishers
|
||||
self.transform = transform
|
||||
}
|
||||
|
||||
/// 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<Downstream>(subscriber: Downstream)
|
||||
where Downstream: Subscriber,
|
||||
Child.Output == Downstream.Input,
|
||||
Upstream.Failure == Downstream.Failure {
|
||||
let inner = Inner(downstream: subscriber,
|
||||
maxPublishers: maxPublishers,
|
||||
transform: transform)
|
||||
upstream.subscribe(inner)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.FlatMap {
|
||||
fileprivate final class Inner<Downstream: Subscriber>
|
||||
: CustomStringConvertible,
|
||||
Cancellable
|
||||
where Downstream.Input == Child.Output, Downstream.Failure == Upstream.Failure {
|
||||
typealias Input = Upstream.Output
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
private typealias PendingValue = (
|
||||
value: Downstream.Input,
|
||||
// If the value was buffered at the time it became available, and the child's
|
||||
// demand was left at `.none` we keep track of the child in `pausedChild` so
|
||||
// that we can demand some more of it after sending this value.
|
||||
pausedChild: ChildSubscriber?)
|
||||
|
||||
private let lock = Lock(recursive: false)
|
||||
private let maxPublishers: Subscribers.Demand
|
||||
private let transform: (Upstream.Output) -> Child
|
||||
// Locking rules for this class.
|
||||
// - All mutable state must only be accessed while `lock` is held.
|
||||
// - In order to avoid any deadlock potential, it is absolutely forbidden to have
|
||||
// any sort of call out from this class while the lock is held. This is why
|
||||
// the draining of the work queue uses a relatively complex pattern.
|
||||
private var downstream: Downstream?
|
||||
private var childSubscribers = Set<ChildSubscriber>()
|
||||
private var downstreamDemand = Subscribers.Demand.unlimited
|
||||
private var valuesToSend = [PendingValue]()
|
||||
private var queueIsBeingProcessed = false
|
||||
private var sendFinishedAfterDrainingQueue = false
|
||||
private var upstreamSubscription: Subscription?
|
||||
|
||||
var description: String { return "FlatMap" }
|
||||
|
||||
init(downstream: Downstream,
|
||||
maxPublishers: Subscribers.Demand,
|
||||
transform: @escaping (Upstream.Output) -> Child) {
|
||||
self.downstream = downstream
|
||||
self.maxPublishers = maxPublishers
|
||||
self.transform = transform
|
||||
}
|
||||
|
||||
final func cancel() {
|
||||
let (upstreamToCancel, childrenToCancel) = lock.do { ()
|
||||
-> (Subscription?, Set<ChildSubscriber>) in
|
||||
let upstreamToCancel = upstreamSubscription
|
||||
upstreamSubscription = nil
|
||||
return (upstreamToCancel, lockedDeactivateAndReturnChildToCancel())
|
||||
}
|
||||
upstreamToCancel?.cancel()
|
||||
cancelChildren(childrenToCancel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Private implementation
|
||||
extension Publishers.FlatMap.Inner {
|
||||
private func deactivate() {
|
||||
cancelChildren(lock.do(lockedDeactivateAndReturnChildToCancel))
|
||||
}
|
||||
|
||||
// Must be called with lock held.
|
||||
private func lockedDeactivateAndReturnChildToCancel() -> Set<ChildSubscriber> {
|
||||
downstream = nil
|
||||
downstreamDemand = .none
|
||||
let result = childSubscribers
|
||||
childSubscribers.removeAll()
|
||||
upstreamSubscription = nil
|
||||
return result
|
||||
}
|
||||
|
||||
private func cancelChildren(_ childrenToCancel: Set<ChildSubscriber>) {
|
||||
childrenToCancel.forEach { $0.cancel() }
|
||||
}
|
||||
|
||||
/// In a thread-safe way, this function performs the passed in work with the lock held
|
||||
/// and then checks to see if either upstream or any of the child subscriptions remain
|
||||
/// active. If there are no remaining active subscriptions, it enqueues the sending
|
||||
/// of `.finished` downstream using the processing queue.
|
||||
/// - Parameter lockedWork: block to be formed with the lock held.
|
||||
private final func maybeSendFinishedAfterExecutingWork(lockedWork: () -> Void) {
|
||||
let shouldProcessQueue: Bool = lock.do {
|
||||
lockedWork()
|
||||
if childSubscribers.isEmpty && upstreamSubscription == nil {
|
||||
sendFinishedAfterDrainingQueue = true
|
||||
if !queueIsBeingProcessed {
|
||||
queueIsBeingProcessed = true
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if shouldProcessQueue {
|
||||
processQueue()
|
||||
}
|
||||
}
|
||||
|
||||
private func receivedCompletion(_ completion: Subscribers.Completion<Failure>,
|
||||
fromChild child: ChildSubscriber) {
|
||||
switch completion {
|
||||
case .finished:
|
||||
removeActiveSubscription(forChild: child)
|
||||
case .failure:
|
||||
downstream?.receive(completion: completion)
|
||||
deactivate()
|
||||
}
|
||||
}
|
||||
|
||||
private func removeActiveSubscription(forChild child: ChildSubscriber) {
|
||||
maybeSendFinishedAfterExecutingWork { childSubscribers.remove(child) }
|
||||
}
|
||||
|
||||
private func receivedValue(_ value: Child.Output,
|
||||
fromChild child: ChildSubscriber) -> Subscribers.Demand {
|
||||
// When receiving a value from a child, we need to determine what additional
|
||||
// demand to return to the child. Apple's logic for this determination is as
|
||||
// follows:
|
||||
// - If we are in `.unlimited` mode, we always request `.none` additional
|
||||
// else
|
||||
// - If there is a surplus relative to the demand, we request `.none`
|
||||
// else
|
||||
// - There is not yet a surplus, so request `.max(1)` more from the child
|
||||
|
||||
let (surplusAvailable, processTheQueue): (Bool, Bool) = lock.do {
|
||||
// If we already have enough values to satisfy the demand, we "buffer" this
|
||||
// child value establishing a surplus.
|
||||
if downstreamDemand <= valuesToSend.count {
|
||||
valuesToSend.append((value, child))
|
||||
return (surplusAvailable: true, processTheQueue: false)
|
||||
} else {
|
||||
valuesToSend.append((value, nil))
|
||||
if queueIsBeingProcessed {
|
||||
return (surplusAvailable: false, processTheQueue: false)
|
||||
}
|
||||
queueIsBeingProcessed = true
|
||||
return (surplusAvailable: false, processTheQueue: true)
|
||||
}
|
||||
}
|
||||
|
||||
let demandResult = surplusAvailable || demandForChild() == .unlimited
|
||||
? Subscribers.Demand.none : Subscribers.Demand.max(1)
|
||||
|
||||
if processTheQueue {
|
||||
processQueue()
|
||||
}
|
||||
|
||||
return demandResult
|
||||
}
|
||||
|
||||
private func demandForChild() -> Subscribers.Demand {
|
||||
return self.downstreamDemand == .unlimited ? .unlimited : .max(1)
|
||||
}
|
||||
|
||||
private enum QueueWorkStatus {
|
||||
case noWork
|
||||
case sendFinish
|
||||
case sendValues(values: ArraySlice<PendingValue>)
|
||||
}
|
||||
|
||||
private func processQueue() {
|
||||
assert(queueIsBeingProcessed)
|
||||
|
||||
// We loop processing the queue in case somebody put stuff on the queue while we
|
||||
// were sending values with the lock unlocked.
|
||||
while true {
|
||||
let work: QueueWorkStatus = lock.do {
|
||||
if downstreamDemand == .none || valuesToSend.isEmpty {
|
||||
if sendFinishedAfterDrainingQueue && valuesToSend.isEmpty {
|
||||
return .sendFinish
|
||||
} else {
|
||||
queueIsBeingProcessed = false
|
||||
return .noWork
|
||||
}
|
||||
}
|
||||
|
||||
let countToSend = min(valuesToSend.count, downstreamDemand.max ?? .max)
|
||||
let result = valuesToSend[0..<countToSend]
|
||||
// TODO: Consider an alternative storage to avoid O(n) removeFirst
|
||||
valuesToSend.removeFirst(countToSend)
|
||||
downstreamDemand -= countToSend
|
||||
return .sendValues(values: result)
|
||||
}
|
||||
|
||||
guard let downstream = downstream else { return }
|
||||
|
||||
switch work {
|
||||
case .noWork:
|
||||
return
|
||||
case .sendFinish:
|
||||
downstream.receive(completion: .finished)
|
||||
deactivate()
|
||||
return
|
||||
case .sendValues(let values):
|
||||
var newDemand = Subscribers.Demand.none
|
||||
values.forEach {
|
||||
newDemand += downstream.receive($0.value)
|
||||
// pausedChild is present only if the value was buffered and the
|
||||
// child's demand was left at `.none`. In that case, once we send the
|
||||
// buffered value, we need to tell the child to get another value.
|
||||
$0.pausedChild?.request(.max(1))
|
||||
}
|
||||
|
||||
if newDemand != .none {
|
||||
lock.do { downstreamDemand += newDemand }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This `Subscriber` implementation is for `FlatMap`'s upstream subscription
|
||||
extension Publishers.FlatMap.Inner: Subscriber {
|
||||
// FlatMap received the upstream subscription
|
||||
fileprivate func receive(subscription: Subscription) {
|
||||
upstreamSubscription = subscription
|
||||
downstream?.receive(subscription: self)
|
||||
subscription.request(maxPublishers)
|
||||
}
|
||||
|
||||
/// Receive a new value from the upstream subscription. A new child subscription
|
||||
/// will be made on the `Child` that the input value is transformed into.
|
||||
/// - Parameter input: a value to be transformed by `transform`
|
||||
fileprivate func receive(_ input: Input) -> Subscribers.Demand {
|
||||
let newChildSubscriber = ChildSubscriber(parent: self)
|
||||
|
||||
lock.do { _ = childSubscribers.insert(newChildSubscriber) }
|
||||
|
||||
self.transform(input).subscribe(newChildSubscriber)
|
||||
|
||||
return .none
|
||||
}
|
||||
|
||||
// Upstream subscription completed
|
||||
fileprivate func receive(completion: Subscribers.Completion<Failure>) {
|
||||
switch completion {
|
||||
case .finished:
|
||||
maybeSendFinishedAfterExecutingWork { upstreamSubscription = nil }
|
||||
case .failure:
|
||||
downstream?.receive(completion: completion)
|
||||
deactivate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Inner is the `Subscription` for `Downstream`
|
||||
extension Publishers.FlatMap.Inner: Subscription {
|
||||
fileprivate func request(_ demand: Subscribers.Demand) {
|
||||
let (drainTheQueue, becameUnlimited) = lock.do { () -> (Bool, Bool) in
|
||||
let becameUnlimited =
|
||||
(demand == .unlimited) && (downstreamDemand != .unlimited)
|
||||
downstreamDemand = demand
|
||||
defer { queueIsBeingProcessed = true }
|
||||
return (!queueIsBeingProcessed, becameUnlimited)
|
||||
}
|
||||
|
||||
if becameUnlimited {
|
||||
// TODO: This code isn't yet thread safe. The correct change is to do this
|
||||
// through the queue just like sending values and finished. Finished is
|
||||
// done through the queue as a bit of a hack. The right design is to have
|
||||
// an enum of actions on the queue. That enum will include (send value,
|
||||
// send finished, set child demand).
|
||||
let newChildDemand = demandForChild()
|
||||
childSubscribers.forEach { $0.request(newChildDemand) }
|
||||
}
|
||||
|
||||
if drainTheQueue {
|
||||
processQueue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.FlatMap.Inner {
|
||||
/// ChildSubscriber is needed to help implement the backpressure/demand strategy.
|
||||
/// Specifically, a custom subscriber is needed to manage the demand of the child
|
||||
/// subscription:
|
||||
/// - Send .max(1) request when the subscription is received
|
||||
/// - Send .max(1) request when downstream subscriber demands more and a previously
|
||||
/// buffered value from the child was sent. (When the value was buffered, the
|
||||
/// child's demand reached .none - effectively pausing the child.)
|
||||
fileprivate final class ChildSubscriber: Hashable {
|
||||
internal typealias Input = Downstream.Input
|
||||
internal typealias Failure = Downstream.Failure
|
||||
|
||||
private var _upstreamSubscription: Subscription?
|
||||
private unowned let _parent: Publishers.FlatMap<Child, Upstream>.Inner<Downstream>
|
||||
|
||||
init(parent: Publishers.FlatMap<Child, Upstream>.Inner<Downstream>) {
|
||||
_parent = parent
|
||||
}
|
||||
|
||||
fileprivate func request(_ demand: Subscribers.Demand) {
|
||||
_upstreamSubscription?.request(demand)
|
||||
}
|
||||
|
||||
public static func == (lhs: ChildSubscriber, rhs: ChildSubscriber) -> Bool {
|
||||
return ObjectIdentifier(lhs) == ObjectIdentifier(rhs)
|
||||
}
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(ObjectIdentifier(self))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.FlatMap.Inner.ChildSubscriber: Cancellable {
|
||||
internal func cancel() {
|
||||
_upstreamSubscription?.cancel()
|
||||
_upstreamSubscription = nil
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.FlatMap.Inner.ChildSubscriber: Subscriber {
|
||||
internal func receive(subscription: Subscription) {
|
||||
if _upstreamSubscription == nil {
|
||||
_upstreamSubscription = subscription
|
||||
subscription.request(_parent.demandForChild())
|
||||
} else {
|
||||
assertionFailure()
|
||||
subscription.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func receive(_ input: Input) -> Subscribers.Demand {
|
||||
return _parent.receivedValue(input, fromChild: self)
|
||||
}
|
||||
|
||||
fileprivate func receive(completion: Subscribers.Completion<Failure>) {
|
||||
_parent.receivedCompletion(completion, fromChild: self)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
//
|
||||
// Publishers.IgnoreOutput.swift
|
||||
//
|
||||
// Created by Eric Patey on 16.08.2019.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Ingores all upstream elements, but passes along a completion
|
||||
/// state (finished or failed).
|
||||
///
|
||||
/// The output type of this publisher is `Never`.
|
||||
/// - Returns: A publisher that ignores all upstream elements.
|
||||
public func ignoreOutput() -> Publishers.IgnoreOutput<Self> {
|
||||
return Publishers.IgnoreOutput(upstream: self)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
/// A publisher that ignores all upstream elements, but passes along a completion
|
||||
/// state (finish or failed).
|
||||
public struct IgnoreOutput<Upstream: Publisher>: Publisher {
|
||||
|
||||
/// The kind of values published by this publisher.
|
||||
public typealias Output = Never
|
||||
|
||||
/// The kind of errors this publisher might publish.
|
||||
///
|
||||
/// Use `Never` if this `Publisher` does not publish errors.
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
public init(upstream: Upstream) {
|
||||
self.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<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Failure == Upstream.Failure, Downstream.Input == Never {
|
||||
let inner = Inner<Downstream>(downstream: subscriber)
|
||||
upstream.subscribe(inner)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.IgnoreOutput {
|
||||
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: OperatorSubscription<Downstream>,
|
||||
Subscriber,
|
||||
CustomStringConvertible,
|
||||
Subscription
|
||||
where Downstream.Input == Never,
|
||||
Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
typealias Output = Never
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
var description: String { return "IgnoreOutput" }
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
upstreamSubscription = subscription
|
||||
downstream.receive(subscription: self)
|
||||
subscription.request(.unlimited)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
return .none
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
// ignore and requests from downstream since we'll never send
|
||||
// any values
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.IgnoreOutput: Equatable where Upstream: Equatable {}
|
||||
@@ -39,7 +39,7 @@ extension Publisher {
|
||||
extension Publishers {
|
||||
/// A publisher that transforms all elements from the upstream publisher with
|
||||
/// a provided closure.
|
||||
public struct Map<Upstream: Publisher, Output> : Publisher {
|
||||
public struct Map<Upstream: Publisher, Output>: Publisher {
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
//
|
||||
// Publishers.ReplaceError.swift
|
||||
// OpenCombine
|
||||
//
|
||||
// Created by Bogdan Vlad on 8/29/19.
|
||||
//
|
||||
|
||||
extension Publishers {
|
||||
/// A publisher that replaces any errors in the stream with a provided element.
|
||||
public struct ReplaceError<Upstream: Publisher>: Publisher {
|
||||
|
||||
/// 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 = Never
|
||||
|
||||
/// The element with which to replace errors from the upstream publisher.
|
||||
public let output: Upstream.Output
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
public init(upstream: Upstream,
|
||||
output: Output) {
|
||||
self.upstream = upstream
|
||||
self.output = output
|
||||
}
|
||||
|
||||
/// 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<SubscriberType: Subscriber>(subscriber: SubscriberType)
|
||||
where Upstream.Output == SubscriberType.Input,
|
||||
SubscriberType.Failure == Failure
|
||||
{
|
||||
let replaceErrorSubscriber = _ReplaceError<Upstream, SubscriberType>(
|
||||
downstream: subscriber,
|
||||
output: output
|
||||
)
|
||||
upstream.subscribe(replaceErrorSubscriber)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publisher {
|
||||
/// Replaces any errors in the stream with the provided element.
|
||||
///
|
||||
/// If the upstream publisher fails with an error, this publisher emits the provided
|
||||
/// element, then finishes normally.
|
||||
/// - Parameter output: An element to emit when the upstream publisher fails.
|
||||
/// - Returns: A publisher that replaces an error from the upstream publisher with
|
||||
/// the provided output element.
|
||||
public func replaceError(with output: Output) -> Publishers.ReplaceError<Self> {
|
||||
return Publishers.ReplaceError(upstream: self, output: output)
|
||||
}
|
||||
}
|
||||
|
||||
private final class _ReplaceError<Upstream: Publisher, Downstream: Subscriber>
|
||||
: OperatorSubscription<Downstream>,
|
||||
Subscriber,
|
||||
CustomStringConvertible,
|
||||
Subscription
|
||||
where Upstream.Output == Downstream.Input
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
var downstreamDemandCounter: Subscribers.Demand = .none
|
||||
var hasFailed: Bool = false
|
||||
var description: String { return "ReplaceError" }
|
||||
|
||||
private let output: Downstream.Input
|
||||
|
||||
init(downstream: Downstream,
|
||||
output: Downstream.Input) {
|
||||
self.output = output
|
||||
super.init(downstream: downstream)
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
if hasFailed {
|
||||
_ = downstream.receive(output)
|
||||
downstream?.receive(completion: .finished)
|
||||
} else {
|
||||
downstreamDemandCounter += demand
|
||||
upstreamSubscription?.request(demand)
|
||||
}
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
upstreamSubscription = subscription
|
||||
downstream.receive(subscription: self)
|
||||
}
|
||||
|
||||
func receive(_ input: Upstream.Output) -> Subscribers.Demand {
|
||||
downstreamDemandCounter -= 1
|
||||
|
||||
let demand = downstream.receive(input)
|
||||
downstreamDemandCounter += demand
|
||||
|
||||
return demand
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Upstream.Failure>) {
|
||||
switch completion {
|
||||
case .finished:
|
||||
downstream.receive(completion: .finished)
|
||||
case .failure:
|
||||
hasFailed = true
|
||||
|
||||
// If there was no demand from downstream,
|
||||
// ReplaceError does not forward the value that
|
||||
// replaces the error until it is requested.
|
||||
if downstreamDemandCounter > 0 {
|
||||
_ = downstream.receive(output)
|
||||
downstream?.receive(completion: .finished)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func cancel() {
|
||||
upstreamSubscription?.cancel()
|
||||
upstreamSubscription = nil
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,6 @@ internal func catching<Input, Output, Failure: Error>(
|
||||
/// an error but returns `Result`.
|
||||
internal func catching<Input, Output>(
|
||||
_ transform: @escaping (Input) throws -> Output
|
||||
) -> (Input) -> Result<Output, Error> {
|
||||
) -> (Input) -> Result<Output, Error> {
|
||||
return { input in Result { try transform(input) } }
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ extension Subscribers {
|
||||
let children: [(label: String?, value: Any)] = [
|
||||
(label: "object", value: object as Any),
|
||||
(label: "keyPath", value: keyPath),
|
||||
(label: "upstreamSubscription", value: _upstreamSubscription as Any)
|
||||
(label: "status", value: _upstreamSubscription as Any)
|
||||
]
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
//
|
||||
// Subscribers.Demand.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 10.06.2019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
import OpenCombineTests
|
||||
|
||||
var tests = [XCTestCaseEntry]()
|
||||
tests += OpenCombineTests.allTests()
|
||||
|
||||
XCTMain(tests)
|
||||
@@ -16,15 +16,6 @@ import OpenCombine
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class AnyCancellableTests: XCTestCase {
|
||||
|
||||
static let allTests = [
|
||||
("testClosureInitialized", testClosureInitialized),
|
||||
("testCancelableInitialized", testCancelableInitialized),
|
||||
("testCancelTwice", testCancelTwice),
|
||||
("testStoreInArbitraryCollection", testStoreInArbitraryCollection),
|
||||
("testStoreInSet", testStoreInSet),
|
||||
("testIndirectCancellation", testIndirectCancellation),
|
||||
]
|
||||
|
||||
func testClosureInitialized() {
|
||||
|
||||
var fired = false
|
||||
|
||||
@@ -16,11 +16,6 @@ import OpenCombine
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class AnyPublisherTests: XCTestCase {
|
||||
|
||||
static let allTests = [
|
||||
("testErasePublisher", testErasePublisher),
|
||||
("testDescription", testDescription),
|
||||
]
|
||||
|
||||
private typealias Sut = AnyPublisher<Int, TestingError>
|
||||
|
||||
func testErasePublisher() {
|
||||
|
||||
@@ -19,16 +19,6 @@ private typealias Sut = AnySubscriber<Int, TestingError>
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class AnySubscriberTests: XCTestCase {
|
||||
|
||||
static let allTests = [
|
||||
("testCombineIdentifier", testCombineIdentifier),
|
||||
("testDescription", testDescription),
|
||||
("testReflection", testReflection),
|
||||
("testErasingSubscriber", testErasingSubscriber),
|
||||
("testErasingSubscriberSubscription", testErasingSubscriberSubscription),
|
||||
("testErasingSubject", testErasingSubject),
|
||||
("testErasingSubjectSubscription", testErasingSubjectSubscription),
|
||||
]
|
||||
|
||||
func testCombineIdentifier() {
|
||||
|
||||
let empty = Sut()
|
||||
@@ -148,18 +138,10 @@ final class AnySubscriberTests: XCTestCase {
|
||||
|
||||
let expectedEvents: [TrackingSubject<Int>.Event] =
|
||||
[.subscription("Subject")] + events.compactMap(subscriberEventToSubjectEvent)
|
||||
.throughFirstCompletion()
|
||||
|
||||
XCTAssertEqual(subject.history, expectedEvents)
|
||||
|
||||
let shuffledEvents = events.shuffled()
|
||||
|
||||
publishEvents(shuffledEvents, erased)
|
||||
|
||||
let expectedShuffledEvents =
|
||||
shuffledEvents.compactMap(subscriberEventToSubjectEvent)
|
||||
|
||||
XCTAssertEqual(subject.history, expectedEvents + expectedShuffledEvents)
|
||||
|
||||
let demand = erased.receive(0)
|
||||
|
||||
XCTAssertEqual(demand, .none)
|
||||
@@ -240,3 +222,21 @@ private func subscriberEventToSubjectEvent(
|
||||
return .completion(c)
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
extension Array {
|
||||
func throughFirstCompletion<SubjectOutput>() -> Array
|
||||
where Element == TrackingSubject<SubjectOutput>.Event
|
||||
{
|
||||
var encounteredFirstCompletion = false
|
||||
return self.prefix {
|
||||
if encounteredFirstCompletion {
|
||||
return false
|
||||
}
|
||||
if case .completion = $0 {
|
||||
encounteredFirstCompletion = true
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,12 +17,6 @@ import OpenCombine
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class CombineIdentifierTests: PerformanceTestCase {
|
||||
|
||||
static let allTests = [
|
||||
("testDefaultInitialized", testDefaultInitialized),
|
||||
("testAnyObject", testAnyObject),
|
||||
("testDefaultInitializedPerformance", testDefaultInitializedPerformance),
|
||||
]
|
||||
|
||||
func testDefaultInitialized() {
|
||||
let id1 = CombineIdentifier()
|
||||
let id2 = CombineIdentifier()
|
||||
|
||||
@@ -13,24 +13,9 @@ import Combine
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
// swiftlint:disable explicit_top_level_acl
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class CurrentValueSubjectTests: XCTestCase {
|
||||
|
||||
static let allTests = [
|
||||
("testRequestingDemand", testRequestingDemand),
|
||||
("testCrashOnZeroInitialDemand", testCrashOnZeroInitialDemand),
|
||||
("testSendFailureCompletion", testSendFailureCompletion),
|
||||
("testMultipleSubscriptions", testMultipleSubscriptions),
|
||||
("testMultipleCompletions", testMultipleCompletions),
|
||||
("testValuesAfterCompletion", testValuesAfterCompletion),
|
||||
("testSubscriptionAfterCompletion", testSubscriptionAfterCompletion),
|
||||
("testSendSubscription", testSendSubscription),
|
||||
("testLifecycle", testLifecycle),
|
||||
("testSynchronization", testSynchronization),
|
||||
]
|
||||
|
||||
private typealias Sut = CurrentValueSubject<Int, TestingError>
|
||||
|
||||
// Reactive Streams Spec: Rules #1, #2, #9
|
||||
@@ -322,6 +307,39 @@ final class CurrentValueSubjectTests: XCTestCase {
|
||||
.completion(.finished)])
|
||||
}
|
||||
|
||||
func testSubscriptionAfterSend() {
|
||||
// Given
|
||||
let passthrough = Sut(0)
|
||||
let subscriber = TrackingSubscriber(
|
||||
receiveSubscription: { subscription in
|
||||
subscription.request(.unlimited)
|
||||
})
|
||||
|
||||
// When
|
||||
passthrough.send(2)
|
||||
passthrough.subscribe(subscriber)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(subscriber.history, [.subscription("CurrentValueSubject"),
|
||||
.value(2)])
|
||||
}
|
||||
|
||||
func testSubscriptionAfterSet() {
|
||||
// Given
|
||||
let passthrough = Sut(0)
|
||||
let subscriber = TrackingSubscriber(receiveSubscription: { subscription in
|
||||
subscription.request(.unlimited)
|
||||
})
|
||||
|
||||
// When
|
||||
passthrough.value = 3
|
||||
passthrough.subscribe(subscriber)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(subscriber.history, [.subscription("CurrentValueSubject"),
|
||||
.value(3)])
|
||||
}
|
||||
|
||||
func testSendSubscription() {
|
||||
let subscription1 = CustomSubscription()
|
||||
let cvs = Sut(1)
|
||||
@@ -441,15 +459,15 @@ final class CurrentValueSubjectTests: XCTestCase {
|
||||
|
||||
race(
|
||||
{
|
||||
cvs.value += 1
|
||||
cvs.value = 42
|
||||
},
|
||||
{
|
||||
cvs.value -= 1
|
||||
cvs.value = 42
|
||||
}
|
||||
)
|
||||
|
||||
XCTAssertEqual(inputs.value.count, 40200)
|
||||
XCTAssertEqual(cvs.value, 112)
|
||||
XCTAssertEqual(cvs.value, 42)
|
||||
|
||||
race(
|
||||
{
|
||||
|
||||
@@ -5,9 +5,7 @@
|
||||
// Created by Joseph Spadafora on 6/29/19.
|
||||
//
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
#if !OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Foundation
|
||||
import OpenCombine
|
||||
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
//
|
||||
// OperatorTestHelper.swift
|
||||
//
|
||||
//
|
||||
// Created by Joseph Spadafora on 7/6/19.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
/// `OperatorTestHelper` is an abstraction that helps avoid a lot of boilerplate when
|
||||
/// testing an operator. It is initialized with a publisher type and creates a
|
||||
/// `CustomSubscription`, `CustomPublisherBase` and `TrackingSubscriberBase`.
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
class OperatorTestHelper<SourceValue: Equatable,
|
||||
SourcePublisher,
|
||||
Sut: Publisher>
|
||||
where Sut.Output: Equatable,
|
||||
SourcePublisher: CustomPublisherBase<SourceValue, TestingError>
|
||||
{
|
||||
typealias Value = Sut.Output
|
||||
typealias Failure = Sut.Failure
|
||||
|
||||
let subscription: CustomSubscription
|
||||
let publisher: SourcePublisher
|
||||
let tracking: TrackingSubscriberBase<Value, Failure>
|
||||
private(set) var sut: Sut
|
||||
|
||||
var downstreamSubscription: Subscription?
|
||||
|
||||
/// This initializes the `OperatorTestHelper`. In most cases,
|
||||
/// you can just pass a `publisherType` and closure
|
||||
/// for `createSut` to get all the setup that you'll need for a test.
|
||||
/// - Parameter publisherType: This should be filled in with the
|
||||
/// type of `CustomPublisherBase` that you would like the
|
||||
/// operator you are testing to be built from.
|
||||
/// - Parameter initialDemand: This is the demand that the
|
||||
/// created `TrackingSubscriber` should return upon receiving a subscription.
|
||||
/// - Parameter receiveValueDemand: This is the demand that the
|
||||
/// created `TrackingSubscriber should return upon receiving a value.
|
||||
/// - Parameter customSubscription: This parameter defaults to `CustomSubscription()`,
|
||||
/// but can be replaced with your own instance if you want to override
|
||||
/// any of the default `CustomSubscription` initializer closures.
|
||||
/// - Parameter createSut: This closure takes a new concrete instance
|
||||
/// of the `publisherType` as an input to the closure and creates an
|
||||
/// instance of the operator that you are trying to test.
|
||||
init(publisherType: SourcePublisher.Type,
|
||||
initialDemand: Subscribers.Demand?,
|
||||
receiveValueDemand: Subscribers.Demand,
|
||||
customSubscription: CustomSubscription = CustomSubscription(),
|
||||
createSut: (SourcePublisher) -> Sut)
|
||||
{
|
||||
self.subscription = customSubscription
|
||||
let createdPublisher = publisherType.init(subscription: customSubscription)
|
||||
self.publisher = createdPublisher
|
||||
self.sut = createSut(createdPublisher)
|
||||
self.tracking = TrackingSubscriberBase<Value, Failure>(
|
||||
receiveSubscription: {
|
||||
initialDemand.map($0.request)
|
||||
},
|
||||
receiveValue: { _ in receiveValueDemand }
|
||||
)
|
||||
tracking.onSubscribe = { self.downstreamSubscription = $0 }
|
||||
sut.subscribe(tracking)
|
||||
}
|
||||
}
|
||||
@@ -86,6 +86,12 @@ final class TrackingSubscriberBase<Value: Equatable, Failure: Error>
|
||||
private let _receiveCompletion: ((Subscribers.Completion<Failure>) -> Void)?
|
||||
private let _onDeinit: (() -> Void)?
|
||||
|
||||
var onSubscribe: ((Subscription) -> Void)?
|
||||
var onValue: ((Input) -> Void)?
|
||||
var onFinish: (() -> Void)?
|
||||
var onFailure: ((Failure) -> Void)?
|
||||
var onDeinit: (() -> Void)?
|
||||
|
||||
/// The history of subscriptions, inputs and completions of this subscriber
|
||||
private(set) var history: [Event] = []
|
||||
|
||||
@@ -144,16 +150,24 @@ final class TrackingSubscriberBase<Value: Equatable, Failure: Error>
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
history.append(.subscription(.init(subscription)))
|
||||
onSubscribe?(subscription)
|
||||
_receiveSubscription?(subscription)
|
||||
}
|
||||
|
||||
func receive(_ input: Value) -> Subscribers.Demand {
|
||||
history.append(.value(input))
|
||||
onValue?(input)
|
||||
return _receiveValue?(input) ?? .none
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
history.append(.completion(completion))
|
||||
switch completion {
|
||||
case .failure(let error):
|
||||
onFailure?(error)
|
||||
case .finished:
|
||||
onFinish?()
|
||||
}
|
||||
_receiveCompletion?(completion)
|
||||
}
|
||||
|
||||
@@ -162,6 +176,7 @@ final class TrackingSubscriberBase<Value: Equatable, Failure: Error>
|
||||
}
|
||||
|
||||
deinit {
|
||||
onDeinit?()
|
||||
_onDeinit?()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,11 +16,6 @@ import OpenCombine
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class ImmediateSchedulerTests: XCTestCase {
|
||||
|
||||
static let allTests = [
|
||||
("testStride", testSchedulerTimeType),
|
||||
("testActions", testActions),
|
||||
]
|
||||
|
||||
func testSchedulerTimeType() throws {
|
||||
|
||||
typealias Stride = ImmediateScheduler.SchedulerTimeType.Stride
|
||||
@@ -80,5 +75,22 @@ final class ImmediateSchedulerTests: XCTestCase {
|
||||
}
|
||||
|
||||
XCTAssertTrue(fired)
|
||||
fired = false
|
||||
|
||||
ImmediateScheduler.shared.schedule(after: ImmediateScheduler.shared.now) {
|
||||
fired = true
|
||||
}
|
||||
|
||||
XCTAssertTrue(fired)
|
||||
fired = false
|
||||
|
||||
let cancellable = ImmediateScheduler
|
||||
.shared
|
||||
.schedule(after: ImmediateScheduler.shared.now, interval: 10) {
|
||||
fired = true
|
||||
}
|
||||
|
||||
XCTAssertTrue(fired)
|
||||
XCTAssertEqual(String(describing: cancellable), "Empty")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,19 +16,6 @@ import OpenCombine
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class PassthroughSubjectTests: XCTestCase {
|
||||
|
||||
static let allTests = [
|
||||
("testRequestingDemand", testRequestingDemand),
|
||||
("testCrashOnZeroInitialDemand", testCrashOnZeroInitialDemand),
|
||||
("testSendFailureCompletion", testSendFailureCompletion),
|
||||
("testMultipleSubscriptions", testMultipleSubscriptions),
|
||||
("testMultipleCompletions", testMultipleCompletions),
|
||||
("testValuesAfterCompletion", testValuesAfterCompletion),
|
||||
("testSubscriptionAfterCompletion", testSubscriptionAfterCompletion),
|
||||
("testSendSubscription", testSendSubscription),
|
||||
("testLifecycle", testLifecycle),
|
||||
("testSynchronization", testSynchronization),
|
||||
]
|
||||
|
||||
private typealias Sut = PassthroughSubject<Int, TestingError>
|
||||
|
||||
// Reactive Streams Spec: Rules #1, #2, #9
|
||||
|
||||
@@ -16,12 +16,6 @@ import OpenCombine
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class PublisherTests: XCTestCase {
|
||||
|
||||
static let allTests = [
|
||||
("testSubscribeSubscriber", testSubscribeSubscriber),
|
||||
("testSubscribeSubject", testSubscribeSubject),
|
||||
("testSubjectSubscriber", testSubjectSubscriber),
|
||||
]
|
||||
|
||||
func testSubscribeSubscriber() {
|
||||
|
||||
final class TrivialPublisher: Publisher {
|
||||
|
||||
@@ -0,0 +1,466 @@
|
||||
//
|
||||
// CompactMapTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 11.07.2019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class CompactMapTests: XCTestCase {
|
||||
|
||||
func testEmpty() {
|
||||
let tracking = TrackingSubscriberBase<Int, TestingError>(
|
||||
receiveSubscription: { $0.request(.unlimited) }
|
||||
)
|
||||
let publisher = TrackingSubject<String>(
|
||||
receiveSubscriber: {
|
||||
XCTAssertEqual(String(describing: $0), "CompactMap")
|
||||
}
|
||||
)
|
||||
|
||||
publisher.compactMap(Int.init).subscribe(tracking)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("CompactMap")])
|
||||
}
|
||||
|
||||
func testError() {
|
||||
let expectedError = TestingError.oops
|
||||
let tracking = TrackingSubscriberBase<Int, TestingError>(
|
||||
receiveSubscription: { $0.request(.unlimited) }
|
||||
)
|
||||
let publisher =
|
||||
CustomPublisherBase<String, TestingError>(subscription: CustomSubscription())
|
||||
|
||||
publisher.compactMap(Int.init).subscribe(tracking)
|
||||
publisher.send(completion: .failure(expectedError))
|
||||
publisher.send(completion: .failure(expectedError))
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("CompactMap"),
|
||||
.completion(.failure(.oops))])
|
||||
}
|
||||
|
||||
func testTryMapFailureBecauseOfThrow() {
|
||||
var counter = 0 // How many times the transform is called?
|
||||
|
||||
let publisher = PassthroughSubject<String, Error>()
|
||||
let compactMap = publisher.tryCompactMap { value -> Int? in
|
||||
counter += 1
|
||||
if value == "throw" {
|
||||
throw "too much" as TestingError
|
||||
}
|
||||
return Int(value)
|
||||
}
|
||||
let tracking = TrackingSubscriberBase<Int, Error>(
|
||||
receiveSubscription: { $0.request(.unlimited) }
|
||||
)
|
||||
|
||||
publisher.send("1")
|
||||
compactMap.subscribe(tracking)
|
||||
publisher.send("2")
|
||||
publisher.send("3")
|
||||
publisher.send("throw")
|
||||
publisher.send("9")
|
||||
publisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(tracking.history,
|
||||
[.subscription("TryCompactMap"),
|
||||
.value(2),
|
||||
.value(3),
|
||||
.completion(.failure("too much" as TestingError))])
|
||||
|
||||
XCTAssertEqual(counter, 3)
|
||||
}
|
||||
|
||||
func testTryMapFailureOnCompletion() {
|
||||
|
||||
let publisher = PassthroughSubject<String, Error>()
|
||||
let compactMap = publisher.tryCompactMap(Int.init)
|
||||
|
||||
let tracking = TrackingSubscriberBase<Int, Error>()
|
||||
|
||||
publisher.send("1")
|
||||
compactMap.subscribe(tracking)
|
||||
publisher.send(completion: .failure(TestingError.oops))
|
||||
publisher.send("2")
|
||||
|
||||
XCTAssertEqual(tracking.history,
|
||||
[.subscription("TryCompactMap"),
|
||||
.completion(.failure(TestingError.oops))])
|
||||
}
|
||||
|
||||
func testRange() {
|
||||
|
||||
let publisher = PassthroughSubject<String, TestingError>()
|
||||
let compactMap = publisher.compactMap(Int.init)
|
||||
let tracking = TrackingSubscriber(receiveSubscription: { $0.request(.unlimited) })
|
||||
|
||||
publisher.send("1")
|
||||
compactMap.subscribe(tracking)
|
||||
publisher.send("2")
|
||||
publisher.send("a")
|
||||
publisher.send("b")
|
||||
publisher.send("a")
|
||||
publisher.send("3")
|
||||
publisher.send("4")
|
||||
publisher.send("5")
|
||||
publisher.send("!")
|
||||
publisher.send(completion: .finished)
|
||||
publisher.send("6")
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("CompactMap"),
|
||||
.value(2),
|
||||
.value(3),
|
||||
.value(4),
|
||||
.value(5),
|
||||
.completion(.finished)])
|
||||
}
|
||||
|
||||
func testNoDemand() {
|
||||
let subscription = CustomSubscription()
|
||||
let publisher =
|
||||
CustomPublisherBase<String, TestingError>(subscription: subscription)
|
||||
let compactMap = publisher.compactMap(Int.init)
|
||||
let tracking = TrackingSubscriber()
|
||||
compactMap.subscribe(tracking)
|
||||
XCTAssertTrue(subscription.history.isEmpty)
|
||||
}
|
||||
|
||||
func testDemandOnSubscribe() {
|
||||
let subscription = CustomSubscription()
|
||||
let publisher =
|
||||
CustomPublisherBase<String, TestingError>(subscription: subscription)
|
||||
let compactMap = publisher.compactMap(Int.init)
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: { $0.request(.max(42)) }
|
||||
)
|
||||
compactMap.subscribe(tracking)
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(42))])
|
||||
}
|
||||
|
||||
func testDemand() {
|
||||
|
||||
let subscription = CustomSubscription()
|
||||
let publisher =
|
||||
CustomPublisherBase<String, TestingError>(subscription: subscription)
|
||||
let compactMap = publisher.compactMap(Int.init)
|
||||
var downstreamSubscription: Subscription?
|
||||
|
||||
var demandOnReceiveValue = Subscribers.Demand.max(3)
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: {
|
||||
$0.request(.max(5))
|
||||
downstreamSubscription = $0
|
||||
},
|
||||
receiveValue: { _ in demandOnReceiveValue }
|
||||
)
|
||||
|
||||
compactMap.subscribe(tracking)
|
||||
|
||||
XCTAssertNotNil(downstreamSubscription)
|
||||
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(5))])
|
||||
// unsatisfied demand = 5
|
||||
|
||||
XCTAssertEqual(publisher.send("a"), .max(1))
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(5))])
|
||||
// unsatisfied demand = 5
|
||||
|
||||
XCTAssertEqual(publisher.send("1"), .max(3))
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(5))])
|
||||
// unsatisfied demand = 5 - 1 + 3 = 7
|
||||
|
||||
demandOnReceiveValue = .max(2)
|
||||
XCTAssertEqual(publisher.send("2"), demandOnReceiveValue)
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(5))])
|
||||
// unsatisfied demand = 7 - 1 + 2 = 8
|
||||
|
||||
demandOnReceiveValue = .max(1)
|
||||
XCTAssertEqual(publisher.send("3"), demandOnReceiveValue)
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(5))])
|
||||
// unsatisfied demand = 8 - 1 + 1 = 8
|
||||
|
||||
XCTAssertEqual(publisher.send("b"), .max(1))
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(5))])
|
||||
// unsatisfied demand = 8
|
||||
|
||||
downstreamSubscription?.request(.max(15))
|
||||
downstreamSubscription?.request(.max(5))
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(5)),
|
||||
.requested(.max(15)),
|
||||
.requested(.max(5))])
|
||||
// unsatisfied demand = 8 + 15 + 5 = 28
|
||||
|
||||
demandOnReceiveValue = .none
|
||||
XCTAssertEqual(publisher.send("4"), demandOnReceiveValue)
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(5)),
|
||||
.requested(.max(15)),
|
||||
.requested(.max(5))])
|
||||
// unsatisfied demand = 28 - 1 + 0 = 27
|
||||
|
||||
downstreamSubscription?.request(.max(121))
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(5)),
|
||||
.requested(.max(15)),
|
||||
.requested(.max(5)),
|
||||
.requested(.max(121))])
|
||||
// unsatisfied demand = 27 + 121 = 148
|
||||
|
||||
XCTAssertEqual(publisher.send("c"), .max(1))
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(5)),
|
||||
.requested(.max(15)),
|
||||
.requested(.max(5)),
|
||||
.requested(.max(121))])
|
||||
// unsatisfied demand = 148
|
||||
|
||||
downstreamSubscription?.cancel()
|
||||
downstreamSubscription?.cancel()
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(5)),
|
||||
.requested(.max(15)),
|
||||
.requested(.max(5)),
|
||||
.requested(.max(121)),
|
||||
.cancelled])
|
||||
downstreamSubscription?.request(.max(3))
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(5)),
|
||||
.requested(.max(15)),
|
||||
.requested(.max(5)),
|
||||
.requested(.max(121)),
|
||||
.cancelled])
|
||||
demandOnReceiveValue = .max(80)
|
||||
XCTAssertEqual(publisher.send("8"), .none)
|
||||
}
|
||||
|
||||
func testCompletion() {
|
||||
let subscription = CustomSubscription()
|
||||
let publisher =
|
||||
CustomPublisherBase<String, TestingError>(subscription: subscription)
|
||||
let compactMap = publisher.compactMap(Int.init)
|
||||
let tracking = TrackingSubscriber(receiveSubscription: { $0.request(.unlimited) })
|
||||
|
||||
compactMap.subscribe(tracking)
|
||||
publisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(tracking.history, [.subscription("CompactMap"),
|
||||
.completion(.finished)])
|
||||
}
|
||||
|
||||
func testCompactMapCancel() throws {
|
||||
let subscription = CustomSubscription()
|
||||
let publisher =
|
||||
CustomPublisherBase<String, TestingError>(subscription: subscription)
|
||||
let compactMap = publisher.compactMap(Int.init)
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: {
|
||||
$0.request(.unlimited)
|
||||
downstreamSubscription = $0
|
||||
}
|
||||
)
|
||||
|
||||
compactMap.subscribe(tracking)
|
||||
try XCTUnwrap(downstreamSubscription).cancel()
|
||||
XCTAssertEqual(publisher.send("1"), .none)
|
||||
publisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited), .cancelled])
|
||||
}
|
||||
|
||||
func testTryCompactMapCancel() throws {
|
||||
let subscription = CustomSubscription()
|
||||
let publisher =
|
||||
CustomPublisherBase<String, TestingError>(subscription: subscription)
|
||||
let tryCompactMap = publisher.tryCompactMap(Int.init)
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriberBase<Int, Error>(
|
||||
receiveSubscription: {
|
||||
$0.request(.unlimited)
|
||||
downstreamSubscription = $0
|
||||
}
|
||||
)
|
||||
|
||||
tryCompactMap.subscribe(tracking)
|
||||
try XCTUnwrap(downstreamSubscription).cancel()
|
||||
XCTAssertEqual(publisher.send("1"), .none)
|
||||
publisher.send(completion: .finished)
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited), .cancelled])
|
||||
}
|
||||
|
||||
func testCancelAlreadyCancelled() throws {
|
||||
let subscription = CustomSubscription()
|
||||
let publisher =
|
||||
CustomPublisherBase<String, TestingError>(subscription: subscription)
|
||||
let compactMap = publisher.compactMap(Int.init)
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: {
|
||||
$0.request(.unlimited)
|
||||
downstreamSubscription = $0
|
||||
}
|
||||
)
|
||||
|
||||
compactMap.subscribe(tracking)
|
||||
try XCTUnwrap(downstreamSubscription).cancel()
|
||||
downstreamSubscription?.request(.unlimited)
|
||||
try XCTUnwrap(downstreamSubscription).cancel()
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited),
|
||||
.cancelled])
|
||||
}
|
||||
|
||||
func testLifecycle() throws {
|
||||
|
||||
var deinitCounter = 0
|
||||
|
||||
let onDeinit = { deinitCounter += 1 }
|
||||
|
||||
do {
|
||||
let passthrough = PassthroughSubject<String, TestingError>()
|
||||
let compactMap = passthrough.compactMap(Int.init)
|
||||
let emptySubscriber = TrackingSubscriber(onDeinit: onDeinit)
|
||||
XCTAssertTrue(emptySubscriber.history.isEmpty)
|
||||
compactMap.subscribe(emptySubscriber)
|
||||
XCTAssertEqual(emptySubscriber.subscriptions.count, 1)
|
||||
passthrough.send("31")
|
||||
XCTAssertEqual(emptySubscriber.inputs.count, 0)
|
||||
passthrough.send(completion: .failure("failure"))
|
||||
XCTAssertEqual(emptySubscriber.completions.count, 1)
|
||||
}
|
||||
|
||||
XCTAssertEqual(deinitCounter, 0)
|
||||
|
||||
do {
|
||||
let passthrough = PassthroughSubject<String, TestingError>()
|
||||
let compactMap = passthrough.compactMap(Int.init)
|
||||
let emptySubscriber = TrackingSubscriber(onDeinit: onDeinit)
|
||||
XCTAssertTrue(emptySubscriber.history.isEmpty)
|
||||
compactMap.subscribe(emptySubscriber)
|
||||
XCTAssertEqual(emptySubscriber.subscriptions.count, 1)
|
||||
XCTAssertEqual(emptySubscriber.inputs.count, 0)
|
||||
XCTAssertEqual(emptySubscriber.completions.count, 0)
|
||||
}
|
||||
|
||||
XCTAssertEqual(deinitCounter, 0)
|
||||
|
||||
var subscription: Subscription?
|
||||
|
||||
do {
|
||||
let passthrough = PassthroughSubject<String, TestingError>()
|
||||
let compactMap = passthrough.compactMap(Int.init)
|
||||
let emptySubscriber = TrackingSubscriber(
|
||||
receiveSubscription: { subscription = $0; $0.request(.unlimited) },
|
||||
onDeinit: onDeinit
|
||||
)
|
||||
XCTAssertTrue(emptySubscriber.history.isEmpty)
|
||||
compactMap.subscribe(emptySubscriber)
|
||||
XCTAssertEqual(emptySubscriber.subscriptions.count, 1)
|
||||
passthrough.send("31")
|
||||
XCTAssertEqual(emptySubscriber.inputs.count, 1)
|
||||
XCTAssertEqual(emptySubscriber.completions.count, 0)
|
||||
XCTAssertNotNil(subscription)
|
||||
}
|
||||
|
||||
XCTAssertEqual(deinitCounter, 0)
|
||||
try XCTUnwrap(subscription).cancel()
|
||||
XCTAssertEqual(deinitCounter, 0)
|
||||
}
|
||||
|
||||
func testCompactMapOperatorSpecializationForCompactMap() {
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: { $0.request(.unlimited) }
|
||||
)
|
||||
let publisher = PassthroughSubject<String, TestingError>()
|
||||
|
||||
let compactMap1 = publisher.compactMap(Int.init)
|
||||
let compactMap2 = compactMap1.compactMap { $0.isMultiple(of: 2) ? $0 / 2 : nil }
|
||||
|
||||
compactMap2.subscribe(tracking)
|
||||
publisher.send("0")
|
||||
publisher.send("3")
|
||||
publisher.send("a")
|
||||
publisher.send("12")
|
||||
publisher.send("11")
|
||||
publisher.send("20")
|
||||
publisher.send("b")
|
||||
publisher.send(completion: .finished)
|
||||
|
||||
XCTAssert(compactMap1.upstream === compactMap2.upstream)
|
||||
XCTAssertEqual(tracking.history, [.subscription("CompactMap"),
|
||||
.value(0),
|
||||
.value(6),
|
||||
.value(10),
|
||||
.completion(.finished)])
|
||||
}
|
||||
|
||||
func testMapOperatorSpecializationForCompactMap() {
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: { $0.request(.unlimited) }
|
||||
)
|
||||
let publisher = PassthroughSubject<String, TestingError>()
|
||||
|
||||
let compactMap1 = publisher.compactMap(Int.init)
|
||||
let compactMap2 = compactMap1.map { $0 + 1 }
|
||||
|
||||
compactMap2.subscribe(tracking)
|
||||
publisher.send("0")
|
||||
publisher.send("3")
|
||||
publisher.send("a")
|
||||
publisher.send("12")
|
||||
publisher.send("11")
|
||||
publisher.send("20")
|
||||
publisher.send("b")
|
||||
publisher.send(completion: .finished)
|
||||
|
||||
XCTAssert(compactMap1.upstream === compactMap2.upstream)
|
||||
XCTAssertEqual(tracking.history, [.subscription("CompactMap"),
|
||||
.value(1),
|
||||
.value(4),
|
||||
.value(13),
|
||||
.value(12),
|
||||
.value(21),
|
||||
.completion(.finished)])
|
||||
}
|
||||
|
||||
func testCompactMapOperatorSpecializationForTryCompactMap() {
|
||||
let tracking = TrackingSubscriberBase<Int, Error>(
|
||||
receiveSubscription: { $0.request(.unlimited) }
|
||||
)
|
||||
let publisher = PassthroughSubject<String, Never>()
|
||||
|
||||
let tryCompactMap1 = publisher.tryCompactMap { input -> Int? in
|
||||
if input == "throw" { throw TestingError.oops }
|
||||
return Int(input)
|
||||
}
|
||||
|
||||
let tryCompactMap2 = tryCompactMap1
|
||||
.compactMap { $0.isMultiple(of: 2) ? $0 / 2 : nil }
|
||||
|
||||
tryCompactMap2.subscribe(tracking)
|
||||
publisher.send("0")
|
||||
publisher.send("3")
|
||||
publisher.send("a")
|
||||
publisher.send("12")
|
||||
publisher.send("11")
|
||||
publisher.send("20")
|
||||
publisher.send("b")
|
||||
|
||||
XCTAssert(tryCompactMap1.upstream === tryCompactMap2.upstream)
|
||||
XCTAssertEqual(tracking.history, [.subscription("TryCompactMap"),
|
||||
.value(0),
|
||||
.value(6),
|
||||
.value(10)])
|
||||
|
||||
publisher.send("throw")
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("TryCompactMap"),
|
||||
.value(0),
|
||||
.value(6),
|
||||
.value(10),
|
||||
.completion(.failure(TestingError.oops))])
|
||||
}
|
||||
}
|
||||
@@ -16,15 +16,6 @@ import OpenCombine
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class CountTests: XCTestCase {
|
||||
|
||||
static let allTests = [
|
||||
("testSendsCorrectCount", testSendsCorrectCount),
|
||||
("testCountWaitsUntilFinishedToSend", testCountWaitsUntilFinishedToSend),
|
||||
("testAddingSubscriberRequestsUnlimitedDemand",
|
||||
testAddingSubscriberRequestsUnlimitedDemand),
|
||||
("testReceivesSubscriptionBeforeRequestingUpstream",
|
||||
testReceivesSubscriptionBeforeRequestingUpstream)
|
||||
]
|
||||
|
||||
func testSendsCorrectCount() {
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
|
||||
@@ -15,11 +15,6 @@ import OpenCombine
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class DecodeTests: XCTestCase {
|
||||
static let allTests = [
|
||||
("testDecodingSuccess", testDecodingSuccess),
|
||||
("testDecodingFailure", testDecodingFailure),
|
||||
("testDemand", testDemand)
|
||||
]
|
||||
|
||||
var jsonEncoder: TestEncoder = TestEncoder()
|
||||
var jsonDecoder: TestDecoder = TestDecoder()
|
||||
@@ -34,7 +29,7 @@ final class DecodeTests: XCTestCase {
|
||||
let data = 78
|
||||
let subject = PassthroughSubject<Int, Error>()
|
||||
let publisher = subject.decode(type: [String : String].self, decoder: jsonDecoder)
|
||||
let subscriber = TrackingSubscriberBase<[String: String], Error>(
|
||||
let subscriber = TrackingSubscriberBase<[String : String], Error>(
|
||||
receiveSubscription: { $0.request(.unlimited) }
|
||||
)
|
||||
jsonDecoder.handleDecode = { decodeData in
|
||||
@@ -57,7 +52,7 @@ final class DecodeTests: XCTestCase {
|
||||
let failData = 95
|
||||
let subject = PassthroughSubject<Int, Error>()
|
||||
let publisher = subject.decode(type: [String : String].self, decoder: jsonDecoder)
|
||||
let subscriber = TrackingSubscriberBase<[String: String], Error>(
|
||||
let subscriber = TrackingSubscriberBase<[String : String], Error>(
|
||||
receiveSubscription: { $0.request(.unlimited) }
|
||||
)
|
||||
|
||||
|
||||
@@ -17,11 +17,6 @@ import OpenCombine
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class DeferredTests: XCTestCase {
|
||||
|
||||
static let allTests = [
|
||||
("testDeferredCreatedAfterSubscription",
|
||||
testDeferredCreatedAfterSubscription)
|
||||
]
|
||||
|
||||
func testDeferredCreatedAfterSubscription() {
|
||||
var deferredPublisherCreatedCount = 0
|
||||
|
||||
|
||||
@@ -16,18 +16,6 @@ import OpenCombine
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class DropWhileTests: XCTestCase {
|
||||
|
||||
static let allTests = [
|
||||
("testDropWhile", testDropWhile),
|
||||
("testTryDropWhileFailureBecauseOfThrow", testTryDropWhileFailureBecauseOfThrow),
|
||||
("testTryDropWhileFailureOnCompletion", testTryDropWhileFailureOnCompletion),
|
||||
("testTryDropWhileSuccess", testTryDropWhileSuccess),
|
||||
("testDemand", testDemand),
|
||||
("testTryDropWhileCancelsUpstreamOnThrow",
|
||||
testTryDropWhileCancelsUpstreamOnThrow),
|
||||
("testDropWhileCompletion",
|
||||
testDropWhileCompletion),
|
||||
]
|
||||
|
||||
func testDropWhile() {
|
||||
|
||||
var counter = 0 // How many times the predicate is called?
|
||||
@@ -237,16 +225,12 @@ final class DropWhileTests: XCTestCase {
|
||||
publisher.send(completion: .finished)
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(tracking.history, [.subscription("DropWhile"),
|
||||
.completion(.finished),
|
||||
.completion(.finished)])
|
||||
|
||||
publisher.send(completion: .failure(.oops))
|
||||
publisher.send(completion: .failure(.oops))
|
||||
XCTAssertEqual(tracking.history, [.subscription("DropWhile"),
|
||||
.completion(.finished),
|
||||
.completion(.finished),
|
||||
.completion(.failure(.oops)),
|
||||
.completion(.failure(.oops))])
|
||||
.completion(.finished)])
|
||||
}
|
||||
|
||||
func testCancelAlreadyCancelled() throws {
|
||||
@@ -271,9 +255,7 @@ final class DropWhileTests: XCTestCase {
|
||||
publisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited), .cancelled])
|
||||
XCTAssertEqual(tracking.history, [.subscription("DropWhile"),
|
||||
.completion(.failure(.oops)),
|
||||
.completion(.finished)])
|
||||
XCTAssertEqual(tracking.history, [.subscription("DropWhile")])
|
||||
}
|
||||
|
||||
func testLifecycle() throws {
|
||||
|
||||
@@ -16,12 +16,6 @@ import OpenCombine
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class EmptyTests: XCTestCase {
|
||||
|
||||
static let allTests = [
|
||||
("testEmpty", testEmpty),
|
||||
("testImmediatelyCancel", testImmediatelyCancel),
|
||||
("testEquatable", testEquatable),
|
||||
]
|
||||
|
||||
func testEmpty() {
|
||||
|
||||
let completesImmediately = Empty(completeImmediately: true,
|
||||
|
||||
@@ -16,13 +16,6 @@ import OpenCombine
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class EncodeTests: XCTestCase {
|
||||
|
||||
static let allTests = [
|
||||
("testEncodingSuccess", testEncodingSuccess),
|
||||
("testEncodingFailure", testEncodingFailure),
|
||||
("testDemand", testDemand),
|
||||
("testEncodeSuccessHistory", testEncodeSuccessHistory),
|
||||
]
|
||||
|
||||
private var encoder = TestEncoder()
|
||||
private var decoder = TestDecoder()
|
||||
|
||||
|
||||
@@ -16,10 +16,6 @@ import OpenCombine
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class FailTests: XCTestCase {
|
||||
|
||||
static let allTests = [
|
||||
("testSubscription", testSubscription),
|
||||
]
|
||||
|
||||
private typealias Sut = Fail<Int, TestingError>
|
||||
|
||||
func testSubscription() {
|
||||
|
||||
@@ -0,0 +1,389 @@
|
||||
//
|
||||
// FilterTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Joseph Spadafora on 6/25/19.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class FilterTests: XCTestCase {
|
||||
|
||||
func testFilterRemovesElements() {
|
||||
// Given
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(2),
|
||||
receiveValueDemand: .none) {
|
||||
$0.filter { $0.isMultiple(of: 2) }
|
||||
}
|
||||
|
||||
// When
|
||||
for i in 1...5 {
|
||||
XCTAssertEqual(helper.publisher.send(i),
|
||||
helper.sut.isIncluded(i) ? .none : .max(1))
|
||||
}
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Filter"),
|
||||
.value(2),
|
||||
.value(4)])
|
||||
}
|
||||
|
||||
func testTryFilterWorks() {
|
||||
// Given
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(2),
|
||||
receiveValueDemand: .none) {
|
||||
$0.tryFilter {
|
||||
try $0.isMultiple(of: 2) && nonthrowingReturn($0)
|
||||
}
|
||||
}
|
||||
|
||||
// When
|
||||
for i in 1...5 {
|
||||
XCTAssertEqual(helper.publisher.send(i),
|
||||
try helper.sut.isIncluded(i) ? .none : .max(1))
|
||||
}
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("TryFilter"),
|
||||
.value(2),
|
||||
.value(4)])
|
||||
}
|
||||
|
||||
func testTryFilterCompletesWithErrorWhenThrown() {
|
||||
// Given
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .none) {
|
||||
$0.tryFilter {
|
||||
try failOnFive(value: $0)
|
||||
}
|
||||
}
|
||||
|
||||
// When
|
||||
for i in 1...5 {
|
||||
_ = helper.publisher.send(i)
|
||||
}
|
||||
|
||||
helper.publisher.send(completion: .finished)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("TryFilter"),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3),
|
||||
.value(4),
|
||||
.completion(.failure(TestingError.oops))
|
||||
])
|
||||
}
|
||||
|
||||
func testCanCompleteWithFinished() {
|
||||
// Given
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .none) {
|
||||
$0.filter { _ in true }
|
||||
}
|
||||
|
||||
// When
|
||||
XCTAssertEqual(helper.publisher.send(1), .none)
|
||||
helper.publisher.send(completion: .finished)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Filter"),
|
||||
.value(1),
|
||||
.completion(.finished)])
|
||||
}
|
||||
|
||||
func testFilterCanCompleteWithError() {
|
||||
// Given
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .none) {
|
||||
$0.filter { _ in true }
|
||||
}
|
||||
|
||||
// When
|
||||
XCTAssertEqual(helper.publisher.send(1), .none)
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Filter"),
|
||||
.value(1),
|
||||
.completion(.failure(.oops))])
|
||||
}
|
||||
|
||||
func testTryFilterCanCompleteWithError() {
|
||||
// Given
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .none,
|
||||
createSut: {
|
||||
$0.tryFilter { _ in true }
|
||||
}
|
||||
)
|
||||
|
||||
// When
|
||||
XCTAssertEqual(helper.publisher.send(1), .none)
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(helper.tracking.history,
|
||||
[.subscription("TryFilter"),
|
||||
.value(1),
|
||||
.completion(.failure(TestingError.oops))])
|
||||
}
|
||||
|
||||
func testFilterSubscriptionDemand() {
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(3),
|
||||
receiveValueDemand: .none,
|
||||
createSut: {
|
||||
$0.filter { $0.isMultiple(of: 2) }
|
||||
}
|
||||
)
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(1), .max(1))
|
||||
XCTAssertEqual(helper.publisher.send(2), .max(0))
|
||||
XCTAssertEqual(helper.publisher.send(3), .max(1))
|
||||
XCTAssertEqual(helper.publisher.send(4), .max(0))
|
||||
XCTAssertEqual(helper.publisher.send(5), .max(1))
|
||||
XCTAssertEqual(helper.publisher.send(6), .max(0))
|
||||
XCTAssertEqual(helper.publisher.send(7), .max(1))
|
||||
XCTAssertEqual(helper.publisher.send(8), .max(0))
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(3))])
|
||||
}
|
||||
|
||||
func testTryFilterSubscriptionDemand() {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(3),
|
||||
receiveValueDemand: .none) {
|
||||
$0.tryFilter { $0.isMultiple(of: 2) }
|
||||
}
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(1), .max(1))
|
||||
XCTAssertEqual(helper.publisher.send(2), .max(0))
|
||||
XCTAssertEqual(helper.publisher.send(3), .max(1))
|
||||
XCTAssertEqual(helper.publisher.send(4), .max(0))
|
||||
XCTAssertEqual(helper.publisher.send(5), .max(1))
|
||||
XCTAssertEqual(helper.publisher.send(6), .max(0))
|
||||
XCTAssertEqual(helper.publisher.send(7), .max(1))
|
||||
XCTAssertEqual(helper.publisher.send(8), .max(0))
|
||||
}
|
||||
|
||||
func testFilterCancel() throws {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .none,
|
||||
createSut: { $0.filter { $0.isMultiple(of: 2) } })
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
XCTAssertEqual(helper.publisher.send(2), .none)
|
||||
helper.publisher.send(completion: .finished)
|
||||
XCTAssertEqual(helper.publisher.send(4), .none)
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited), .cancelled])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Filter")])
|
||||
}
|
||||
|
||||
func testTryFilterCancel() throws {
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .none,
|
||||
createSut: {
|
||||
$0.tryFilter { try failOnFive(value: $0) && $0.isMultiple(of: 2) }
|
||||
}
|
||||
)
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
XCTAssertEqual(helper.publisher.send(2), .none)
|
||||
helper.publisher.send(completion: .finished)
|
||||
XCTAssertEqual(helper.publisher.send(4), .none)
|
||||
XCTAssertEqual(helper.publisher.send(5), .none)
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited), .cancelled])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("TryFilter")])
|
||||
}
|
||||
|
||||
func testCancelAlreadyCancelled() throws {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .none,
|
||||
createSut: { $0.filter { $0.isMultiple(of: 2) } })
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.unlimited)
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited), .cancelled])
|
||||
}
|
||||
|
||||
func testLifecycle() throws {
|
||||
|
||||
var deinitCounter = 0
|
||||
|
||||
let onDeinit = { deinitCounter += 1 }
|
||||
|
||||
do {
|
||||
let passthrough = PassthroughSubject<Int, TestingError>()
|
||||
let filter = passthrough.filter { $0.isMultiple(of: 2) }
|
||||
let emptySubscriber = TrackingSubscriber(onDeinit: onDeinit)
|
||||
XCTAssertTrue(emptySubscriber.history.isEmpty)
|
||||
filter.subscribe(emptySubscriber)
|
||||
XCTAssertEqual(emptySubscriber.subscriptions.count, 1)
|
||||
passthrough.send(31)
|
||||
XCTAssertEqual(emptySubscriber.inputs.count, 0)
|
||||
passthrough.send(completion: .failure("failure"))
|
||||
XCTAssertEqual(emptySubscriber.completions.count, 1)
|
||||
}
|
||||
|
||||
XCTAssertEqual(deinitCounter, 0)
|
||||
|
||||
do {
|
||||
let passthrough = PassthroughSubject<Int, TestingError>()
|
||||
let filter = passthrough.filter { $0.isMultiple(of: 2) }
|
||||
let emptySubscriber = TrackingSubscriber(onDeinit: onDeinit)
|
||||
XCTAssertTrue(emptySubscriber.history.isEmpty)
|
||||
filter.subscribe(emptySubscriber)
|
||||
XCTAssertEqual(emptySubscriber.subscriptions.count, 1)
|
||||
XCTAssertEqual(emptySubscriber.inputs.count, 0)
|
||||
XCTAssertEqual(emptySubscriber.completions.count, 0)
|
||||
}
|
||||
|
||||
XCTAssertEqual(deinitCounter, 0)
|
||||
|
||||
var subscription: Subscription?
|
||||
|
||||
do {
|
||||
let passthrough = PassthroughSubject<Int, TestingError>()
|
||||
let filter = passthrough.filter { $0.isMultiple(of: 2) }
|
||||
let emptySubscriber = TrackingSubscriber(
|
||||
receiveSubscription: { subscription = $0; $0.request(.unlimited) },
|
||||
onDeinit: onDeinit
|
||||
)
|
||||
XCTAssertTrue(emptySubscriber.history.isEmpty)
|
||||
filter.subscribe(emptySubscriber)
|
||||
XCTAssertEqual(emptySubscriber.subscriptions.count, 1)
|
||||
passthrough.send(32)
|
||||
XCTAssertEqual(emptySubscriber.inputs.count, 1)
|
||||
XCTAssertEqual(emptySubscriber.completions.count, 0)
|
||||
XCTAssertNotNil(subscription)
|
||||
}
|
||||
|
||||
XCTAssertEqual(deinitCounter, 0)
|
||||
try XCTUnwrap(subscription).cancel()
|
||||
XCTAssertEqual(deinitCounter, 0)
|
||||
}
|
||||
|
||||
func testFilterOperatorSpecializationForFilter() {
|
||||
// Given
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(1),
|
||||
receiveValueDemand: .none) {
|
||||
$0.filter {
|
||||
$0.isMultiple(of: 3)
|
||||
}.filter {
|
||||
$0.isMultiple(of: 5)
|
||||
}
|
||||
}
|
||||
|
||||
// When
|
||||
for i in 1...20 {
|
||||
XCTAssertEqual(helper.publisher.send(i),
|
||||
helper.sut.isIncluded(i) ? .none : .max(1))
|
||||
}
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Filter"), .value(15)])
|
||||
}
|
||||
|
||||
func testTryFilterOperatorSpecializationForFilter() {
|
||||
// Given
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(1),
|
||||
receiveValueDemand: .none) {
|
||||
$0.filter {
|
||||
$0.isMultiple(of: 3)
|
||||
}.tryFilter {
|
||||
$0.isMultiple(of: 5)
|
||||
}
|
||||
}
|
||||
|
||||
// When
|
||||
for i in 1...20 {
|
||||
XCTAssertEqual(helper.publisher.send(i),
|
||||
try helper.sut.isIncluded(i) ? .none : .max(1))
|
||||
}
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("TryFilter"), .value(15)])
|
||||
}
|
||||
|
||||
func testFilterOperatorSpecializationForTryFilter() {
|
||||
// Given
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(1),
|
||||
receiveValueDemand: .none) {
|
||||
$0.tryFilter {
|
||||
$0.isMultiple(of: 3)
|
||||
}.filter {
|
||||
$0.isMultiple(of: 5)
|
||||
}
|
||||
}
|
||||
|
||||
// When
|
||||
for i in 1...20 {
|
||||
XCTAssertEqual(helper.publisher.send(i),
|
||||
try helper.sut.isIncluded(i) ? .none : .max(1))
|
||||
}
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("TryFilter"), .value(15)])
|
||||
}
|
||||
|
||||
func testTryFilterOperatorSpecializationForTryFilter() {
|
||||
// Given
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(3),
|
||||
receiveValueDemand: .none) {
|
||||
$0.tryFilter {
|
||||
$0.isMultiple(of: 3)
|
||||
}.tryFilter {
|
||||
$0.isMultiple(of: 5)
|
||||
}
|
||||
}
|
||||
|
||||
// When
|
||||
for i in 1...20 {
|
||||
XCTAssertEqual(helper.publisher.send(i),
|
||||
try helper.sut.isIncluded(i) ? .none : .max(1))
|
||||
}
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("TryFilter"),
|
||||
.value(15)])
|
||||
}
|
||||
}
|
||||
|
||||
private func nonthrowingReturn(_ value: Int) throws -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
private func failOnFive(value: Int) throws -> Bool {
|
||||
if value == 5 {
|
||||
throw TestingError.oops
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,606 @@
|
||||
//
|
||||
// FirstTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Joseph Spadafora on 7/9/19.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class FirstTests: XCTestCase {
|
||||
|
||||
func testFirstDemand() throws {
|
||||
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .none,
|
||||
createSut: { $0.first() })
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("First")])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(1), .none)
|
||||
XCTAssertEqual(helper.publisher.send(2), .none)
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("First")])
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.unlimited)
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(1))
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited), .cancelled])
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("First"),
|
||||
.value(1),
|
||||
.completion(.finished)])
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited), .cancelled])
|
||||
}
|
||||
|
||||
func testFirstFinishesAndReturnsFirstItem() {
|
||||
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(3),
|
||||
receiveValueDemand: .max(1),
|
||||
createSut: { $0.first() })
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("First")])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(25), .none)
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("First"),
|
||||
.value(25),
|
||||
.completion(.finished)])
|
||||
|
||||
helper.publisher.send(completion: .finished)
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("First"),
|
||||
.value(25),
|
||||
.completion(.finished)])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(73), .none)
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("First"),
|
||||
.value(25),
|
||||
.completion(.finished)])
|
||||
}
|
||||
|
||||
func testFirstFinishesWithError() {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(3),
|
||||
receiveValueDemand: .max(1),
|
||||
createSut: { $0.first() })
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("First")])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("First"),
|
||||
.completion(.failure(.oops))])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("First"),
|
||||
.completion(.failure(.oops))])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(73), .none)
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("First"),
|
||||
.completion(.failure(.oops))])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
}
|
||||
|
||||
func testFirstFinishesImmediately() {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(3),
|
||||
receiveValueDemand: .max(1),
|
||||
createSut: { $0.first() })
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("First")])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
|
||||
helper.publisher.send(completion: .finished)
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("First"),
|
||||
.completion(.finished)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("First"),
|
||||
.completion(.finished)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(73), .none)
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("First"),
|
||||
.completion(.finished)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
}
|
||||
|
||||
func testFirstLifecycle() throws {
|
||||
|
||||
var deinitCounter = 0
|
||||
|
||||
let onDeinit = { deinitCounter += 1 }
|
||||
|
||||
do {
|
||||
let passthrough = PassthroughSubject<Int, TestingError>()
|
||||
let first = passthrough.first()
|
||||
let emptySubscriber = TrackingSubscriber(onDeinit: onDeinit)
|
||||
XCTAssertTrue(emptySubscriber.history.isEmpty)
|
||||
first.subscribe(emptySubscriber)
|
||||
XCTAssertEqual(emptySubscriber.subscriptions.count, 1)
|
||||
passthrough.send(31)
|
||||
XCTAssertEqual(emptySubscriber.inputs.count, 0)
|
||||
XCTAssertEqual(emptySubscriber.completions.count, 0)
|
||||
}
|
||||
|
||||
XCTAssertEqual(deinitCounter, 0)
|
||||
|
||||
do {
|
||||
let passthrough = PassthroughSubject<Int, TestingError>()
|
||||
let first = passthrough.first()
|
||||
let emptySubscriber = TrackingSubscriber(onDeinit: onDeinit)
|
||||
XCTAssertTrue(emptySubscriber.history.isEmpty)
|
||||
first.subscribe(emptySubscriber)
|
||||
XCTAssertEqual(emptySubscriber.subscriptions.count, 1)
|
||||
XCTAssertEqual(emptySubscriber.inputs.count, 0)
|
||||
XCTAssertEqual(emptySubscriber.completions.count, 0)
|
||||
}
|
||||
|
||||
XCTAssertEqual(deinitCounter, 0)
|
||||
|
||||
var subscription: Subscription?
|
||||
|
||||
do {
|
||||
let passthrough = PassthroughSubject<Int, TestingError>()
|
||||
let first = passthrough.first()
|
||||
let emptySubscriber = TrackingSubscriber(
|
||||
receiveSubscription: { subscription = $0; $0.request(.unlimited) },
|
||||
onDeinit: onDeinit
|
||||
)
|
||||
XCTAssertTrue(emptySubscriber.history.isEmpty)
|
||||
first.subscribe(emptySubscriber)
|
||||
XCTAssertEqual(emptySubscriber.subscriptions.count, 1)
|
||||
passthrough.send(32)
|
||||
XCTAssertEqual(emptySubscriber.inputs.count, 1)
|
||||
XCTAssertEqual(emptySubscriber.completions.count, 1)
|
||||
XCTAssertNotNil(subscription)
|
||||
}
|
||||
|
||||
XCTAssertEqual(deinitCounter, 0)
|
||||
try XCTUnwrap(subscription).cancel()
|
||||
XCTAssertEqual(deinitCounter, 0)
|
||||
}
|
||||
|
||||
func testFirstWhereDemand() throws {
|
||||
|
||||
var firedCounter = 0
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .none,
|
||||
createSut: {
|
||||
$0.first {
|
||||
firedCounter += 1
|
||||
return $0 > 1
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("TryFirst")])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(0), .none)
|
||||
XCTAssertEqual(helper.publisher.send(1), .none)
|
||||
XCTAssertEqual(helper.publisher.send(2), .none)
|
||||
XCTAssertEqual(helper.publisher.send(3), .none)
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("TryFirst")])
|
||||
XCTAssertEqual(firedCounter, 3)
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.unlimited)
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(1))
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited), .cancelled])
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("TryFirst"),
|
||||
.value(2),
|
||||
.completion(.finished)])
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited), .cancelled])
|
||||
}
|
||||
|
||||
func testFirstWhereFinishesAndReturnsFirstMatchingItem() {
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(5),
|
||||
receiveValueDemand: .max(1),
|
||||
createSut: { $0.first(where: { $0 > 2 }) }
|
||||
)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("TryFirst")])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(1), .none)
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("TryFirst")])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(2), .none)
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("TryFirst")])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(3), .none)
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("TryFirst"),
|
||||
.value(3),
|
||||
.completion(.finished)])
|
||||
|
||||
helper.publisher.send(completion: .finished)
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("TryFirst"),
|
||||
.value(3),
|
||||
.completion(.finished)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited), .cancelled])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(4), .none)
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("TryFirst"),
|
||||
.value(3),
|
||||
.completion(.finished)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited), .cancelled])
|
||||
}
|
||||
|
||||
func testFirstWhereFinishesWithError() {
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(5),
|
||||
receiveValueDemand: .max(1),
|
||||
createSut: { $0.first(where: { $0 > 2 }) }
|
||||
)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("TryFirst")])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("TryFirst"),
|
||||
.completion(.failure(.oops))])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("TryFirst"),
|
||||
.completion(.failure(.oops))])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(73), .none)
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("TryFirst"),
|
||||
.completion(.failure(.oops))])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
}
|
||||
|
||||
func testFirstWhereLifecycle() throws {
|
||||
|
||||
var deinitCounter = 0
|
||||
let onDeinit = { deinitCounter += 1 }
|
||||
|
||||
do {
|
||||
let passthrough = PassthroughSubject<Int, TestingError>()
|
||||
let firstWhere = passthrough.first { $0 > 1 }
|
||||
|
||||
let emptySubscriber = TrackingSubscriber(onDeinit: onDeinit)
|
||||
XCTAssertTrue(emptySubscriber.history.isEmpty)
|
||||
firstWhere.subscribe(emptySubscriber)
|
||||
XCTAssertEqual(emptySubscriber.subscriptions.count, 1)
|
||||
passthrough.send(31)
|
||||
XCTAssertEqual(emptySubscriber.inputs.count, 0)
|
||||
XCTAssertEqual(emptySubscriber.completions.count, 0)
|
||||
}
|
||||
|
||||
XCTAssertEqual(deinitCounter, 0)
|
||||
|
||||
do {
|
||||
let passthrough = PassthroughSubject<Int, TestingError>()
|
||||
let firstWhere = passthrough.first { $0 > 1 }
|
||||
let emptySubscriber = TrackingSubscriber(onDeinit: onDeinit)
|
||||
XCTAssertTrue(emptySubscriber.history.isEmpty)
|
||||
firstWhere.subscribe(emptySubscriber)
|
||||
XCTAssertEqual(emptySubscriber.subscriptions.count, 1)
|
||||
XCTAssertEqual(emptySubscriber.inputs.count, 0)
|
||||
XCTAssertEqual(emptySubscriber.completions.count, 0)
|
||||
}
|
||||
|
||||
XCTAssertEqual(deinitCounter, 0)
|
||||
|
||||
var subscription: Subscription?
|
||||
|
||||
do {
|
||||
let passthrough = PassthroughSubject<Int, TestingError>()
|
||||
let firstWhere = passthrough.first { $0 > 1 }
|
||||
let emptySubscriber = TrackingSubscriber(
|
||||
receiveSubscription: { subscription = $0; $0.request(.unlimited) },
|
||||
onDeinit: onDeinit
|
||||
)
|
||||
XCTAssertTrue(emptySubscriber.history.isEmpty)
|
||||
firstWhere.subscribe(emptySubscriber)
|
||||
XCTAssertEqual(emptySubscriber.subscriptions.count, 1)
|
||||
passthrough.send(32)
|
||||
XCTAssertEqual(emptySubscriber.inputs.count, 1)
|
||||
XCTAssertEqual(emptySubscriber.completions.count, 1)
|
||||
XCTAssertNotNil(subscription)
|
||||
}
|
||||
|
||||
XCTAssertEqual(deinitCounter, 0)
|
||||
try XCTUnwrap(subscription).cancel()
|
||||
XCTAssertEqual(deinitCounter, 0)
|
||||
|
||||
var predicateDeinitCounter = 0
|
||||
let onPredicateDeinit = {
|
||||
predicateDeinitCounter += 1
|
||||
}
|
||||
|
||||
do {
|
||||
let passthrough = PassthroughSubject<Int, TestingError>()
|
||||
let firstWhere = passthrough.first { _ in
|
||||
_ = TrackingSubscriber(onDeinit: onPredicateDeinit)
|
||||
return true
|
||||
}
|
||||
|
||||
XCTAssertEqual(predicateDeinitCounter, 0)
|
||||
|
||||
let subscriber =
|
||||
TrackingSubscriber(receiveSubscription: { $0.request(.max(1)) })
|
||||
XCTAssertTrue(subscriber.history.isEmpty)
|
||||
firstWhere.subscribe(subscriber)
|
||||
XCTAssertEqual(subscriber.subscriptions.count, 1)
|
||||
passthrough.send(31)
|
||||
XCTAssertEqual(subscriber.inputs.count, 1)
|
||||
XCTAssertEqual(subscriber.completions.count, 1)
|
||||
XCTAssertEqual(predicateDeinitCounter, 1)
|
||||
}
|
||||
}
|
||||
|
||||
func testTryFirstWhereDemand() throws {
|
||||
|
||||
var firedCounter = 0
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .none,
|
||||
createSut: {
|
||||
$0.tryFirst {
|
||||
firedCounter += 1
|
||||
return $0 > 1
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("TryFirstWhere")])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(0), .none)
|
||||
XCTAssertEqual(helper.publisher.send(1), .none)
|
||||
XCTAssertEqual(helper.publisher.send(2), .none)
|
||||
XCTAssertEqual(helper.publisher.send(3), .none)
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("TryFirstWhere")])
|
||||
XCTAssertEqual(firedCounter, 3)
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.unlimited)
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(1))
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited), .cancelled])
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("TryFirstWhere"),
|
||||
.value(2),
|
||||
.completion(.finished)])
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited), .cancelled])
|
||||
}
|
||||
|
||||
func testTryFirstWhereReturnsFirstMatchingElement() {
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(5),
|
||||
receiveValueDemand: .max(1),
|
||||
createSut: { $0.tryFirst(where: { $0 > 6 }) }
|
||||
)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("TryFirstWhere")])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
|
||||
for number in 1...6 {
|
||||
XCTAssertEqual(helper.publisher.send(number), .none)
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("TryFirstWhere")])
|
||||
}
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(7), .none)
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("TryFirstWhere"),
|
||||
.value(7),
|
||||
.completion(.finished)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited), .cancelled])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(8), .none)
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("TryFirstWhere"),
|
||||
.value(7),
|
||||
.completion(.finished)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited), .cancelled])
|
||||
}
|
||||
|
||||
func testTryFirstWhereFinishesWithError() {
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(5),
|
||||
receiveValueDemand: .max(1),
|
||||
createSut: { $0.tryFirst(where: { $0 > 6 }) }
|
||||
)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("TryFirstWhere")])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
XCTAssertEqual(helper.tracking.history,
|
||||
[.subscription("TryFirstWhere"),
|
||||
.completion(.failure(TestingError.oops))])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
XCTAssertEqual(helper.tracking.history,
|
||||
[.subscription("TryFirstWhere"),
|
||||
.completion(.failure(TestingError.oops))])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(73), .none)
|
||||
XCTAssertEqual(helper.tracking.history,
|
||||
[.subscription("TryFirstWhere"),
|
||||
.completion(.failure(TestingError.oops))])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
}
|
||||
|
||||
func testTryFirstWhereFinishesWhenErrorThrown() {
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(5),
|
||||
receiveValueDemand: .max(1),
|
||||
createSut: {
|
||||
$0.tryFirst(where: {
|
||||
if $0 == 3 {
|
||||
throw TestingError.oops
|
||||
}
|
||||
return $0 > 3
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("TryFirstWhere")])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(2), .none)
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("TryFirstWhere")])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(3), .none)
|
||||
XCTAssertEqual(helper.tracking.history,
|
||||
[.subscription("TryFirstWhere"),
|
||||
.completion(.failure(TestingError.oops))])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited), .cancelled])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(4), .none)
|
||||
XCTAssertEqual(helper.tracking.history,
|
||||
[.subscription("TryFirstWhere"),
|
||||
.completion(.failure(TestingError.oops))])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited), .cancelled])
|
||||
}
|
||||
|
||||
func testTryFirstWhereLifecycle() throws {
|
||||
|
||||
var deinitCounter = 0
|
||||
let onDeinit = { deinitCounter += 1 }
|
||||
|
||||
do {
|
||||
let passthrough = PassthroughSubject<Int, TestingError>()
|
||||
let tryFirstWhere = passthrough.tryFirst { $0 > 1 }
|
||||
|
||||
let emptySubscriber = TrackingSubscriberBase<Int, Error>(onDeinit: onDeinit)
|
||||
XCTAssertTrue(emptySubscriber.history.isEmpty)
|
||||
tryFirstWhere.subscribe(emptySubscriber)
|
||||
XCTAssertEqual(emptySubscriber.subscriptions.count, 1)
|
||||
passthrough.send(31)
|
||||
XCTAssertEqual(emptySubscriber.inputs.count, 0)
|
||||
XCTAssertEqual(emptySubscriber.completions.count, 0)
|
||||
}
|
||||
|
||||
XCTAssertEqual(deinitCounter, 0)
|
||||
|
||||
do {
|
||||
let passthrough = PassthroughSubject<Int, TestingError>()
|
||||
let tryFirstWhere = passthrough.tryFirst { $0 > 1 }
|
||||
let emptySubscriber = TrackingSubscriberBase<Int, Error>(onDeinit: onDeinit)
|
||||
XCTAssertTrue(emptySubscriber.history.isEmpty)
|
||||
tryFirstWhere.subscribe(emptySubscriber)
|
||||
XCTAssertEqual(emptySubscriber.subscriptions.count, 1)
|
||||
XCTAssertEqual(emptySubscriber.inputs.count, 0)
|
||||
XCTAssertEqual(emptySubscriber.completions.count, 0)
|
||||
}
|
||||
|
||||
XCTAssertEqual(deinitCounter, 0)
|
||||
|
||||
var subscription: Subscription?
|
||||
|
||||
do {
|
||||
let passthrough = PassthroughSubject<Int, TestingError>()
|
||||
let tryFirstWhere = passthrough.tryFirst { $0 > 1 }
|
||||
let emptySubscriber = TrackingSubscriberBase<Int, Error>(
|
||||
receiveSubscription: { subscription = $0; $0.request(.unlimited) },
|
||||
onDeinit: onDeinit
|
||||
)
|
||||
XCTAssertTrue(emptySubscriber.history.isEmpty)
|
||||
tryFirstWhere.subscribe(emptySubscriber)
|
||||
XCTAssertEqual(emptySubscriber.subscriptions.count, 1)
|
||||
passthrough.send(32)
|
||||
XCTAssertEqual(emptySubscriber.inputs.count, 1)
|
||||
XCTAssertEqual(emptySubscriber.completions.count, 1)
|
||||
XCTAssertNotNil(subscription)
|
||||
}
|
||||
|
||||
XCTAssertEqual(deinitCounter, 0)
|
||||
try XCTUnwrap(subscription).cancel()
|
||||
XCTAssertEqual(deinitCounter, 0)
|
||||
|
||||
var predicateDeinitCounter = 0
|
||||
let onPredicateDeinit = {
|
||||
predicateDeinitCounter += 1
|
||||
}
|
||||
|
||||
do {
|
||||
let passthrough = PassthroughSubject<Int, TestingError>()
|
||||
let tryFirstWhere = passthrough.tryFirst { _ in
|
||||
_ = TrackingSubscriber(onDeinit: onPredicateDeinit)
|
||||
return true
|
||||
}
|
||||
|
||||
XCTAssertEqual(predicateDeinitCounter, 0)
|
||||
|
||||
let subscriber = TrackingSubscriberBase<Int, Error>(
|
||||
receiveSubscription: { $0.request(.max(1)) }
|
||||
)
|
||||
XCTAssertTrue(subscriber.history.isEmpty)
|
||||
tryFirstWhere.subscribe(subscriber)
|
||||
XCTAssertEqual(subscriber.subscriptions.count, 1)
|
||||
passthrough.send(31)
|
||||
XCTAssertEqual(subscriber.inputs.count, 1)
|
||||
XCTAssertEqual(subscriber.completions.count, 1)
|
||||
XCTAssertEqual(predicateDeinitCounter, 1)
|
||||
}
|
||||
|
||||
do {
|
||||
let passthrough = PassthroughSubject<Int, TestingError>()
|
||||
let tryFirstWhere = passthrough.tryFirst { _ in
|
||||
_ = TrackingSubscriber(onDeinit: onPredicateDeinit)
|
||||
throw TestingError.oops
|
||||
}
|
||||
|
||||
XCTAssertEqual(predicateDeinitCounter, 1)
|
||||
|
||||
let subscriber = TrackingSubscriberBase<Int, Error>(
|
||||
receiveSubscription: { $0.request(.max(1)) }
|
||||
)
|
||||
XCTAssertTrue(subscriber.history.isEmpty)
|
||||
tryFirstWhere.subscribe(subscriber)
|
||||
XCTAssertEqual(subscriber.subscriptions.count, 1)
|
||||
passthrough.send(31)
|
||||
XCTAssertEqual(subscriber.inputs.count, 0)
|
||||
XCTAssertEqual(subscriber.completions.count, 1)
|
||||
XCTAssertEqual(predicateDeinitCounter, 2)
|
||||
}
|
||||
}
|
||||
|
||||
func testCancelAlreadyCancelled() throws {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .none,
|
||||
createSut: { $0.first() })
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.unlimited)
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited), .cancelled])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,625 @@
|
||||
//
|
||||
// FlatMapTests.swift
|
||||
//
|
||||
// Created by Eric Patey on 17.08.2019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
/// Helper function for predictably testing concurrency/race scenarios.
|
||||
/// - Parameter block: block to execute concurrently
|
||||
///
|
||||
/// Apple, surprisingly, calls out to subscribers with a lock held. This will absolutely
|
||||
/// block children who send values concurrently until the current downstream value
|
||||
/// delivery has been completed.
|
||||
///
|
||||
/// This means that, without a timeout, the code below is guaranteed to deadlock. Because
|
||||
/// of this we need to choose a timeout that is low enough to not materially slow down the
|
||||
/// tests, but long enough to ensure that we are effectively testing the desired race
|
||||
/// conditions. It needs to be a long enough timeout to allow the caller's block to begin
|
||||
/// executing.
|
||||
///
|
||||
/// I understand that timeouts like this are a smell. I'd be happy to entertain other ways
|
||||
/// to deterministically test concurrency/race conditions.
|
||||
|
||||
func performConcurrentBlock(_ block: @escaping () -> Void) {
|
||||
let sem = DispatchSemaphore(value: 0)
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
block()
|
||||
sem.signal()
|
||||
}
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
// If running in compatibility mode, assert that we got a timeout. If not, Apple
|
||||
// changed their implementation to not call out with a lock held.
|
||||
XCTAssertEqual(sem.wait(timeout: DispatchTime.now() + 0.01), .timedOut)
|
||||
#else
|
||||
sem.wait()
|
||||
#endif
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class FlatMapTests: XCTestCase {
|
||||
|
||||
static let allTests = [
|
||||
("testSendsChildValues", testSendsChildValues),
|
||||
("testChildSubscribeDeadlock", testChildSubscribeDeadlock),
|
||||
("testCancelCancels", testCancelCancels),
|
||||
("testUpstreamDemandWithMaxPublishers", testUpstreamDemandWithMaxPublishers),
|
||||
("testUpstreamDemandWithNoMaxPublishers", testUpstreamDemandWithNoMaxPublishers),
|
||||
("testChildDemandWhenUnlimited", testChildDemandWhenUnlimited),
|
||||
("testChildDemandWhenLimited", testChildDemandWhenLimited),
|
||||
("testDemandFromLimitedtoUnlimited", testDemandFromLimitedToUnlimited),
|
||||
("testChildValueReceivedWhileSendingValue",
|
||||
testChildValueReceivedWhileSendingValue),
|
||||
("testCompletesProperlyWhenChildrenOutliveUpstream",
|
||||
testCompletesProperlyWhenChildrenOutliveUpstream),
|
||||
("testCompletesProperlyWhenUpstreamOutlivesChildren",
|
||||
testCompletesProperlyWhenUpstreamOutlivesChildren),
|
||||
("testDoesNotCompleteWithBufferedValues", testDoesNotCompleteWithBufferedValues),
|
||||
("testFailsIfUpstreamFails", testFailsIfUpstreamFails),
|
||||
("testFailsIfChildFails", testFailsIfChildFails),
|
||||
("testFailsWithoutSendingBufferedValues", testFailsWithoutSendingBufferedValues),
|
||||
("testAllSubscriptionsReleasedOnUpstreamFailure",
|
||||
testAllSubscriptionsReleasedOnUpstreamFailure),
|
||||
("testAllSubscriptionsReleasedOnChildFailure",
|
||||
testAllSubscriptionsReleasedOnChildFailure),
|
||||
("testSendsSubcriptionDownstreamBeforeDemandUpstream",
|
||||
testSendsSubcriptionDownstreamBeforeDemandUpstream),
|
||||
("testTestSuiteIncludesAllTests", testTestSuiteIncludesAllTests)
|
||||
]
|
||||
|
||||
func testSendsChildValues() {
|
||||
let upstreamPublisher = PassthroughSubject<
|
||||
PassthroughSubject<Int, TestingError>,
|
||||
TestingError>()
|
||||
let childPublisher1 = PassthroughSubject<Int, TestingError>()
|
||||
let childPublisher2 = PassthroughSubject<Int, TestingError>()
|
||||
|
||||
let flatMap = upstreamPublisher.flatMap { $0 }
|
||||
|
||||
let downstreamSubscriber = TrackingSubscriber(receiveSubscription: {
|
||||
$0.request(.unlimited)
|
||||
})
|
||||
|
||||
flatMap.subscribe(downstreamSubscriber)
|
||||
|
||||
upstreamPublisher.send(childPublisher1)
|
||||
upstreamPublisher.send(childPublisher2)
|
||||
|
||||
childPublisher1.send(666)
|
||||
childPublisher2.send(777)
|
||||
XCTAssertEqual(downstreamSubscriber.history, [.subscription("FlatMap"),
|
||||
.value(666),
|
||||
.value(777)])
|
||||
}
|
||||
|
||||
// This test ensures that the code can properly re-enter when synchronously receiving
|
||||
// a value during subscription (which Just(_) does).
|
||||
// 1. FlatMap.Inner.receive(_ input:)
|
||||
// 2. Publisher.subscribe
|
||||
// ...
|
||||
// 3. FlatMap.Inner.ChildSubscriber.recive(subscription:)
|
||||
// 4. subscription.request()
|
||||
// 5. Just.Inner.request()
|
||||
// 6. FlatMap.Inner.child(_:receivedValue)
|
||||
// 7. lock
|
||||
//
|
||||
// At one point, I had a bug where the lock was taken by #1 before calling #2
|
||||
// This broke the rules of calling out with a lock held, and lead to a deadlock
|
||||
// at #7.
|
||||
//
|
||||
// Also, in my opinion, working around the issue with recursive locks is a smell.
|
||||
func testChildSubscribeDeadlock() {
|
||||
let upstreamSubscription = CustomSubscription()
|
||||
let upstreamPublisher = CustomPublisherBase<Int, Never>(
|
||||
subscription: upstreamSubscription)
|
||||
|
||||
let flatMap = upstreamPublisher.flatMap(maxPublishers: .max(2)) { Just($0) }
|
||||
|
||||
let downstreamSubscriber = TrackingSubscriberBase<Int, Never>(
|
||||
receiveSubscription: { $0.request(.unlimited) })
|
||||
|
||||
flatMap.subscribe(downstreamSubscriber)
|
||||
XCTAssertEqual(upstreamPublisher.send(666), .none)
|
||||
|
||||
// Simply making it here shows that there's no dealock
|
||||
}
|
||||
|
||||
func testCancelCancels() {
|
||||
let upstreamSubscription = CustomSubscription()
|
||||
let upstreamPublisher = CustomPublisherBase<Int, Never>(
|
||||
subscription: upstreamSubscription)
|
||||
|
||||
let childSubscription = CustomSubscription()
|
||||
let childPublisher = CustomPublisherBase<Int, Never>(
|
||||
subscription: childSubscription)
|
||||
|
||||
let flatMap = upstreamPublisher.flatMap { _ in childPublisher }
|
||||
|
||||
var downstreamSubscription: Subscription?
|
||||
let downstreamSubscriber = TrackingSubscriberBase<Int, Never>(receiveSubscription:
|
||||
{
|
||||
downstreamSubscription = $0
|
||||
$0.request(.unlimited)
|
||||
})
|
||||
|
||||
flatMap.subscribe(downstreamSubscriber)
|
||||
|
||||
XCTAssertEqual(upstreamPublisher.send(1), .none)
|
||||
|
||||
downstreamSubscription?.cancel()
|
||||
|
||||
XCTAssertEqual(upstreamSubscription.history.last, .cancelled)
|
||||
XCTAssertEqual(childSubscription.history.last, .cancelled)
|
||||
}
|
||||
|
||||
func testUpstreamDemandWithMaxPublishers() {
|
||||
var upstreamDemand = Subscribers.Demand.none
|
||||
let upstreamSubscription = CustomSubscription(onRequest: { upstreamDemand += $0 })
|
||||
let upstreamPublisher = CustomPublisherBase<Int, Never>(
|
||||
subscription: upstreamSubscription)
|
||||
|
||||
let flatMap = upstreamPublisher.flatMap(maxPublishers: .max(2)) { Just($0) }
|
||||
|
||||
let downstreamSubscriber = TrackingSubscriberBase<Int, Never>(
|
||||
receiveSubscription: { $0.request(.unlimited) })
|
||||
|
||||
flatMap.subscribe(downstreamSubscriber)
|
||||
|
||||
XCTAssertEqual(upstreamDemand, .max(2))
|
||||
}
|
||||
|
||||
func testUpstreamDemandWithNoMaxPublishers() {
|
||||
var upstreamDemand = Subscribers.Demand.none
|
||||
let upstreamSubscription = CustomSubscription(onRequest: { upstreamDemand += $0 })
|
||||
let upstreamPublisher = CustomPublisherBase<Int, Never>(
|
||||
subscription: upstreamSubscription)
|
||||
|
||||
let flatMap = upstreamPublisher.flatMap { Just($0) }
|
||||
|
||||
let downstreamSubscriber = TrackingSubscriberBase<Int, Never>(
|
||||
receiveSubscription: { $0.request(.unlimited) })
|
||||
|
||||
flatMap.subscribe(downstreamSubscriber)
|
||||
|
||||
XCTAssertEqual(upstreamDemand, .unlimited)
|
||||
}
|
||||
|
||||
func testChildDemandWhenUnlimited() throws {
|
||||
let upstreamPublisher = PassthroughSubject<Void, Never>()
|
||||
|
||||
var childDemand = Subscribers.Demand.none
|
||||
let childSubscription = CustomSubscription(onRequest: { childDemand += $0 })
|
||||
let childPublisher = CustomPublisherBase<Int, Never>(
|
||||
subscription: childSubscription)
|
||||
|
||||
let flatMap = upstreamPublisher.flatMap { _ in childPublisher }
|
||||
|
||||
let downstreamSubscriber = TrackingSubscriberBase<Int, Never>(receiveSubscription:
|
||||
{
|
||||
$0.request(.unlimited)
|
||||
})
|
||||
|
||||
flatMap.subscribe(downstreamSubscriber)
|
||||
|
||||
upstreamPublisher.send()
|
||||
|
||||
XCTAssertEqual(childDemand, .unlimited)
|
||||
|
||||
XCTAssertEqual(childPublisher.send(666), .none)
|
||||
}
|
||||
|
||||
func testChildDemandWhenLimited() throws {
|
||||
let upstreamPublisher = PassthroughSubject<AnyPublisher<Int, Never>, Never>()
|
||||
|
||||
var child1Demand = Subscribers.Demand.none
|
||||
let child1Subscription = CustomSubscription(onRequest: { child1Demand += $0 })
|
||||
let child1Publisher = CustomPublisherBase<Int, Never>(
|
||||
subscription: child1Subscription)
|
||||
|
||||
var child2Demand = Subscribers.Demand.none
|
||||
let child2Subscription = CustomSubscription(onRequest: { child2Demand += $0 })
|
||||
let child2Publisher = CustomPublisherBase<Int, Never>(
|
||||
subscription: child2Subscription)
|
||||
|
||||
let flatMap = upstreamPublisher.flatMap { $0 }
|
||||
|
||||
var downstreamSubscription: Subscription?
|
||||
let downstreamSubscriber = TrackingSubscriberBase<Int, Never>(receiveSubscription:
|
||||
{
|
||||
downstreamSubscription = $0
|
||||
$0.request(.max(2))
|
||||
})
|
||||
|
||||
flatMap.subscribe(downstreamSubscriber)
|
||||
|
||||
upstreamPublisher.send(AnyPublisher(child1Publisher))
|
||||
upstreamPublisher.send(AnyPublisher(child2Publisher))
|
||||
|
||||
// Apple starts out the children with a demand of 1. On receipt of a child value,
|
||||
// 1 more is demanded until it has one extra/buffered value after the downstream
|
||||
// demand is satisfied.
|
||||
XCTAssertEqual(child1Demand, .max(1))
|
||||
XCTAssertEqual(child2Demand, .max(1))
|
||||
|
||||
// Downstream demand is 2, so:
|
||||
// - this value gets sent
|
||||
// - downstream demand goes down to 1
|
||||
// - child is asked for 1 more
|
||||
XCTAssertEqual(child1Publisher.send(666), .max(1))
|
||||
XCTAssertEqual(downstreamSubscriber.history, [.subscription("FlatMap"),
|
||||
.value(666)])
|
||||
|
||||
// Downstream demand is 1, so:
|
||||
// - this value gets sent
|
||||
// - downstream demand goes down to 0, but still need a buffered value
|
||||
// - child is asked for 1 more
|
||||
XCTAssertEqual(child1Publisher.send(777), .max(1))
|
||||
XCTAssertEqual(downstreamSubscriber.history, [.subscription("FlatMap"),
|
||||
.value(666),
|
||||
.value(777)])
|
||||
|
||||
// Downstream demand is 0, so:
|
||||
// - this value is buffered and NOT sent
|
||||
// - downstream demand is 0 and there's a buffered value
|
||||
// - child is asked for 0 more
|
||||
XCTAssertEqual(child1Publisher.send(888), .none)
|
||||
XCTAssertEqual(downstreamSubscriber.history, [.subscription("FlatMap"),
|
||||
.value(666),
|
||||
.value(777)])
|
||||
|
||||
XCTAssertEqual(child1Publisher.send(999), .none)
|
||||
XCTAssertEqual(downstreamSubscriber.history, [.subscription("FlatMap"),
|
||||
.value(666),
|
||||
.value(777)])
|
||||
|
||||
// Downstream demands more, so:
|
||||
// - the buffered value gets sent
|
||||
// - child is asked for 1 more
|
||||
XCTAssertEqual(child1Demand, .max(1))
|
||||
XCTAssertEqual(child2Demand, .max(1))
|
||||
try XCTUnwrap(downstreamSubscription).request(.max(10))
|
||||
// This is a little odd, but rather than re-establishing a demand of 1 on the
|
||||
// child like it did initially, Apple appears to demand 1 of the child for every
|
||||
// buffered value that was sent. In this case, child1 is asked for 2 more.
|
||||
XCTAssertEqual(child1Demand, .max(3))
|
||||
XCTAssertEqual(child2Demand, .max(1))
|
||||
XCTAssertEqual(downstreamSubscriber.history, [.subscription("FlatMap"),
|
||||
.value(666),
|
||||
.value(777),
|
||||
.value(888),
|
||||
.value(999)])
|
||||
}
|
||||
|
||||
func testDemandFromLimitedToUnlimited() {
|
||||
let upstreamPublisher = PassthroughSubject<Void, Never>()
|
||||
|
||||
var childDemand = Subscribers.Demand.none
|
||||
let childSubscription = CustomSubscription(onRequest: { childDemand += $0 })
|
||||
let childPublisher = CustomPublisherBase<Int, Never>(
|
||||
subscription: childSubscription)
|
||||
|
||||
let flatMap = upstreamPublisher.flatMap { _ in childPublisher }
|
||||
|
||||
var downstreamSubscription: Subscription?
|
||||
let downstreamSubscriber = TrackingSubscriberBase<Int, Never>(receiveSubscription:
|
||||
{
|
||||
downstreamSubscription = $0
|
||||
$0.request(.max(3))
|
||||
})
|
||||
|
||||
flatMap.subscribe(downstreamSubscriber)
|
||||
|
||||
upstreamPublisher.send()
|
||||
|
||||
XCTAssertEqual(childDemand, .max(1))
|
||||
|
||||
downstreamSubscription?.request(.unlimited)
|
||||
XCTAssertEqual(childDemand, .unlimited)
|
||||
}
|
||||
|
||||
func testChildValueReceivedWhileSendingValue() throws {
|
||||
let upstreamPublisher = PassthroughSubject<AnyPublisher<Int, TestingError>,
|
||||
TestingError>()
|
||||
|
||||
let child1Publisher = CustomPublisher(subscription: CustomSubscription())
|
||||
let child2Publisher = CustomPublisher(subscription: CustomSubscription())
|
||||
|
||||
let flatMap = upstreamPublisher.flatMap { $0 }
|
||||
|
||||
let received777Sem = DispatchSemaphore(value: 0)
|
||||
|
||||
let downstreamSubscriber = TrackingSubscriber(
|
||||
receiveSubscription: { $0.request(.max(2)) },
|
||||
receiveValue: {
|
||||
if $0 == 666 {
|
||||
performConcurrentBlock {
|
||||
XCTAssertEqual(child2Publisher.send(777), .max(1))
|
||||
}
|
||||
} else if $0 == 777 {
|
||||
received777Sem.signal()
|
||||
}
|
||||
return .none
|
||||
}
|
||||
)
|
||||
|
||||
flatMap.subscribe(downstreamSubscriber)
|
||||
|
||||
upstreamPublisher.send(AnyPublisher(child1Publisher))
|
||||
upstreamPublisher.send(AnyPublisher(child2Publisher))
|
||||
|
||||
XCTAssertEqual(child1Publisher.send(666), .max(1))
|
||||
received777Sem.wait()
|
||||
XCTAssertEqual(downstreamSubscriber.history, [.subscription("FlatMap"),
|
||||
.value(666),
|
||||
.value(777)])
|
||||
}
|
||||
|
||||
func testCompletesProperlyWhenUpstreamOutlivesChildren() {
|
||||
let upstreamPublisher = PassthroughSubject<AnyPublisher<Int, Never>, Never>()
|
||||
let child1 = PassthroughSubject<Int, Never>()
|
||||
let child2 = PassthroughSubject<Int, Never>()
|
||||
let flatMap = upstreamPublisher.flatMap { $0 }
|
||||
let downstreamSubscriber = TrackingSubscriberBase<Int, Never>(
|
||||
receiveSubscription: { $0.request(.unlimited) })
|
||||
|
||||
flatMap.subscribe(downstreamSubscriber)
|
||||
|
||||
upstreamPublisher.send(AnyPublisher(child1))
|
||||
upstreamPublisher.send(AnyPublisher(child2))
|
||||
|
||||
XCTAssertEqual(downstreamSubscriber.history, [.subscription("FlatMap")])
|
||||
|
||||
child1.send(666)
|
||||
child1.send(completion: .finished)
|
||||
|
||||
// Better stay alive even after upstream and one child finished
|
||||
XCTAssertEqual(downstreamSubscriber.history, [.subscription("FlatMap"),
|
||||
.value(666)])
|
||||
|
||||
child2.send(777)
|
||||
child2.send(completion: .finished)
|
||||
|
||||
// Better stay alive even after all children finished
|
||||
XCTAssertEqual(downstreamSubscriber.history, [.subscription("FlatMap"),
|
||||
.value(666),
|
||||
.value(777)])
|
||||
|
||||
upstreamPublisher.send(completion: .finished)
|
||||
|
||||
// Better complete when upstream and all children finished
|
||||
XCTAssertEqual(downstreamSubscriber.history, [.subscription("FlatMap"),
|
||||
.value(666),
|
||||
.value(777),
|
||||
.completion(.finished)])
|
||||
}
|
||||
|
||||
func testCompletesProperlyWhenChildrenOutliveUpstream() {
|
||||
let upstreamPublisher = PassthroughSubject<AnyPublisher<Int, Never>, Never>()
|
||||
let child1 = PassthroughSubject<Int, Never>()
|
||||
let child2 = PassthroughSubject<Int, Never>()
|
||||
let flatMap = upstreamPublisher.flatMap { $0 }
|
||||
let downstreamSubscriber = TrackingSubscriberBase<Int, Never>(
|
||||
receiveSubscription: { $0.request(.unlimited) })
|
||||
|
||||
flatMap.subscribe(downstreamSubscriber)
|
||||
|
||||
upstreamPublisher.send(AnyPublisher(child1))
|
||||
upstreamPublisher.send(AnyPublisher(child2))
|
||||
|
||||
XCTAssertEqual(downstreamSubscriber.history, [.subscription("FlatMap")])
|
||||
|
||||
upstreamPublisher.send(completion: .finished)
|
||||
|
||||
// Better stay alive even after upstream finished
|
||||
XCTAssertEqual(downstreamSubscriber.history, [.subscription("FlatMap")])
|
||||
|
||||
child1.send(666)
|
||||
child1.send(completion: .finished)
|
||||
|
||||
// Better stay alive even after upstream and one child finished
|
||||
XCTAssertEqual(downstreamSubscriber.history, [.subscription("FlatMap"),
|
||||
.value(666)])
|
||||
|
||||
child2.send(777)
|
||||
child2.send(completion: .finished)
|
||||
|
||||
// Better complete when upstream and all children finished
|
||||
XCTAssertEqual(downstreamSubscriber.history, [.subscription("FlatMap"),
|
||||
.value(666),
|
||||
.value(777),
|
||||
.completion(.finished)])
|
||||
}
|
||||
|
||||
func testDoesNotCompleteWithBufferedValues() {
|
||||
let upstreamPublisher = PassthroughSubject<Void, Never>()
|
||||
|
||||
let childSubscription = CustomSubscription()
|
||||
let childPublisher = CustomPublisherBase<Int, Never>(
|
||||
subscription: childSubscription)
|
||||
|
||||
let flatMap = upstreamPublisher.flatMap { _ in childPublisher }
|
||||
|
||||
var downstreamSubscription: Subscription?
|
||||
let downstreamSubscriber = TrackingSubscriberBase<Int, Never>(receiveSubscription:
|
||||
{
|
||||
downstreamSubscription = $0
|
||||
$0.request(.max(1))
|
||||
})
|
||||
|
||||
flatMap.subscribe(downstreamSubscriber)
|
||||
|
||||
upstreamPublisher.send()
|
||||
|
||||
XCTAssertEqual(childPublisher.send(666), .max(1))
|
||||
XCTAssertEqual(childPublisher.send(777), .none)
|
||||
XCTAssertEqual(downstreamSubscriber.history, [.subscription("FlatMap"),
|
||||
.value(666)])
|
||||
|
||||
upstreamPublisher.send(completion: .finished)
|
||||
childPublisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(downstreamSubscriber.history, [.subscription("FlatMap"),
|
||||
.value(666)])
|
||||
|
||||
downstreamSubscription?.request(.unlimited)
|
||||
XCTAssertEqual(downstreamSubscriber.history, [.subscription("FlatMap"),
|
||||
.value(666),
|
||||
.value(777),
|
||||
.completion(.finished)])
|
||||
}
|
||||
|
||||
func testFailsIfUpstreamFails() {
|
||||
let upstreamPublisher = PassthroughSubject<
|
||||
AnyPublisher<Int, TestingError>,
|
||||
TestingError>()
|
||||
let flatMap = upstreamPublisher.flatMap { $0 }
|
||||
let downstreamSubscriber = TrackingSubscriberBase<Int, TestingError>(
|
||||
receiveSubscription: { $0.request(.unlimited) })
|
||||
|
||||
flatMap.subscribe(downstreamSubscriber)
|
||||
|
||||
upstreamPublisher.send(completion: .failure(TestingError.oops))
|
||||
|
||||
XCTAssertEqual(downstreamSubscriber.history, [.subscription("FlatMap"),
|
||||
.completion(.failure(TestingError.oops))])
|
||||
}
|
||||
|
||||
func testFailsIfChildFails() {
|
||||
let upstream = PassthroughSubject<AnyPublisher<Int, TestingError>, TestingError>()
|
||||
let child = PassthroughSubject<Int, TestingError>()
|
||||
let flatMap = upstream.flatMap { $0 }
|
||||
let tracking = TrackingSubscriberBase<Int, TestingError>(
|
||||
receiveSubscription: { $0.request(.unlimited) })
|
||||
|
||||
flatMap.subscribe(tracking)
|
||||
upstream.send(AnyPublisher(child))
|
||||
|
||||
child.send(completion: .failure(TestingError.oops))
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("FlatMap"),
|
||||
.completion(.failure(TestingError.oops))])
|
||||
}
|
||||
|
||||
func testFailsWithoutSendingBufferedValues() {
|
||||
let upstreamPublisher = PassthroughSubject<
|
||||
PassthroughSubject<Int, TestingError>,
|
||||
TestingError>()
|
||||
let childPublisher = PassthroughSubject<Int, TestingError>()
|
||||
|
||||
let flatMap = upstreamPublisher.flatMap { $0 }
|
||||
|
||||
let downstreamSubscriber = TrackingSubscriber(receiveSubscription: {
|
||||
$0.request(.max(1))
|
||||
})
|
||||
|
||||
flatMap.subscribe(downstreamSubscriber)
|
||||
|
||||
upstreamPublisher.send(childPublisher)
|
||||
|
||||
// Send a value
|
||||
childPublisher.send(666)
|
||||
XCTAssertEqual(downstreamSubscriber.history, [.subscription("FlatMap"),
|
||||
.value(666)])
|
||||
|
||||
// Buffer a value
|
||||
childPublisher.send(777)
|
||||
XCTAssertEqual(downstreamSubscriber.history, [.subscription("FlatMap"),
|
||||
.value(666)])
|
||||
|
||||
// Fail
|
||||
let error = TestingError.oops
|
||||
upstreamPublisher.send(completion: .failure(error))
|
||||
XCTAssertEqual(downstreamSubscriber.history, [.subscription("FlatMap"),
|
||||
.value(666),
|
||||
.completion(.failure(error))])
|
||||
}
|
||||
|
||||
func testAllSubscriptionsReleasedOnUpstreamFailure() {
|
||||
let upstreamSubscription = CustomSubscription()
|
||||
let upstreamPublisher = CustomPublisherBase<Int, TestingError>(
|
||||
subscription: upstreamSubscription)
|
||||
let downstreamSubscriber = TrackingSubscriberBase<Int, TestingError>(
|
||||
receiveSubscription: { $0.request(.unlimited) })
|
||||
|
||||
let childSubscription = CustomSubscription()
|
||||
let child = CustomPublisher(subscription: childSubscription)
|
||||
|
||||
let flatMap = upstreamPublisher.flatMap { _ in child }
|
||||
|
||||
flatMap.subscribe(downstreamSubscriber)
|
||||
|
||||
XCTAssertEqual(upstreamPublisher.send(1), .none)
|
||||
XCTAssertEqual(child.send(666), .none)
|
||||
upstreamPublisher.send(completion: .failure(TestingError.oops))
|
||||
|
||||
XCTAssertEqual(childSubscription.history, [.requested(.unlimited),
|
||||
.cancelled])
|
||||
XCTAssertEqual(upstreamSubscription.history, [.requested(.unlimited)])
|
||||
}
|
||||
|
||||
func testAllSubscriptionsReleasedOnChildFailure() {
|
||||
let upstreamSubscription = CustomSubscription()
|
||||
let upstreamPublisher = CustomPublisherBase<Int, TestingError>(
|
||||
subscription: upstreamSubscription)
|
||||
let downstreamSubscriber = TrackingSubscriberBase<Int, TestingError>(
|
||||
receiveSubscription: { $0.request(.unlimited) })
|
||||
|
||||
let child1 = PassthroughSubject<Int, TestingError>()
|
||||
let child2Subscription = CustomSubscription()
|
||||
let child2 = CustomPublisher(subscription: child2Subscription)
|
||||
|
||||
let children = [AnyPublisher(child1), AnyPublisher(child2)]
|
||||
let flatMap = upstreamPublisher.flatMap { children[$0] }
|
||||
|
||||
flatMap.subscribe(downstreamSubscriber)
|
||||
|
||||
XCTAssertEqual(upstreamPublisher.send(0), .none)
|
||||
XCTAssertEqual(upstreamPublisher.send(1), .none)
|
||||
child1.send(666)
|
||||
XCTAssertEqual(child2.send(777), .none)
|
||||
child1.send(completion: .failure(TestingError.oops))
|
||||
|
||||
XCTAssertEqual(child2Subscription.history, [.requested(.unlimited),
|
||||
.cancelled])
|
||||
XCTAssertEqual(upstreamSubscription.history, [.requested(.unlimited)])
|
||||
}
|
||||
|
||||
func testSendsSubcriptionDownstreamBeforeDemandUpstream() {
|
||||
let sentDemandRequestUpstream = "Sent demand request upstream"
|
||||
let sentSubscriptionDownstream = "Sent subcription downstream"
|
||||
var receiveOrder: [String] = []
|
||||
|
||||
let upstreamSubscription = CustomSubscription(onRequest: { _ in
|
||||
receiveOrder.append(sentDemandRequestUpstream) })
|
||||
let upstreamPublisher = CustomPublisherBase<Int, Never>(
|
||||
subscription: upstreamSubscription)
|
||||
let flatMapPublisher = upstreamPublisher.flatMap { Just($0) }
|
||||
let downstreamSubscriber = TrackingSubscriberBase<Int, Never>(
|
||||
receiveSubscription: { _ in receiveOrder.append(sentSubscriptionDownstream) })
|
||||
|
||||
flatMapPublisher.subscribe(downstreamSubscriber)
|
||||
|
||||
XCTAssertEqual(receiveOrder, [sentSubscriptionDownstream,
|
||||
sentDemandRequestUpstream])
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
func testTestSuiteIncludesAllTests() {
|
||||
// https://oleb.net/blog/2017/03/keeping-xctest-in-sync/
|
||||
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
|
||||
let thisClass = type(of: self)
|
||||
let allTestsCount = thisClass.allTests.count
|
||||
let darwinCount = thisClass.defaultTestSuite.testCaseCount
|
||||
XCTAssertEqual(allTestsCount,
|
||||
darwinCount,
|
||||
"\(darwinCount - allTestsCount) tests are missing from allTests")
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
//
|
||||
// IgnoreOutputTests.swift
|
||||
//
|
||||
// Created by Eric Patey on 16.08.20019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class IgnoreOutputTests: XCTestCase {
|
||||
|
||||
func testCompletionWithEmpty() {
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let ignoreOutputPublisher = publisher.ignoreOutput()
|
||||
let tracking = TrackingSubscriberBase<Never, TestingError>(
|
||||
receiveSubscription: { $0.request(.max(42)) }
|
||||
)
|
||||
|
||||
XCTAssertEqual(tracking.history, [])
|
||||
|
||||
ignoreOutputPublisher.subscribe(tracking)
|
||||
XCTAssertEqual(tracking.history, [.subscription("IgnoreOutput")])
|
||||
|
||||
publisher.send(completion: .finished)
|
||||
XCTAssertEqual(tracking.history, [.subscription("IgnoreOutput"),
|
||||
.completion(.finished)])
|
||||
}
|
||||
|
||||
func testCompletionWithValues() {
|
||||
let upstreamSubscription = CustomSubscription()
|
||||
let upstreamPublisher = CustomPublisher(subscription: upstreamSubscription)
|
||||
let ignoreOutputPublisher = upstreamPublisher.ignoreOutput()
|
||||
let tracking = TrackingSubscriberBase<Never, TestingError>(
|
||||
receiveSubscription: { $0.request(.max(42)) }
|
||||
)
|
||||
|
||||
XCTAssertEqual(tracking.history, [])
|
||||
|
||||
ignoreOutputPublisher.subscribe(tracking)
|
||||
XCTAssertEqual(tracking.history, [.subscription("IgnoreOutput")])
|
||||
|
||||
XCTAssertEqual(upstreamPublisher.send(666), .none)
|
||||
XCTAssertEqual(tracking.history, [.subscription("IgnoreOutput")])
|
||||
|
||||
upstreamPublisher.send(completion: .finished)
|
||||
XCTAssertEqual(tracking.history, [.subscription("IgnoreOutput"),
|
||||
.completion(.finished)])
|
||||
}
|
||||
|
||||
func testCompletionWithError() {
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let ignoreOutputPublisher = publisher.ignoreOutput()
|
||||
let tracking = TrackingSubscriberBase<Never, TestingError>(
|
||||
receiveSubscription: { $0.request(.max(42)) }
|
||||
)
|
||||
|
||||
XCTAssertEqual(tracking.history, [])
|
||||
|
||||
ignoreOutputPublisher.subscribe(tracking)
|
||||
XCTAssertEqual(tracking.history, [.subscription("IgnoreOutput")])
|
||||
|
||||
XCTAssertEqual(publisher.send(666), .none)
|
||||
XCTAssertEqual(tracking.history, [.subscription("IgnoreOutput")])
|
||||
|
||||
publisher.send(completion: .failure(.oops))
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("IgnoreOutput"),
|
||||
.completion(.failure(.oops))])
|
||||
}
|
||||
|
||||
func testDemand() {
|
||||
// demand from downstream is ignored since no values are ever
|
||||
// sent. upstream demand is set to unlimited and left alone.
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let ignoreOutput = publisher.ignoreOutput()
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriberBase<Never, TestingError>(
|
||||
receiveSubscription: {
|
||||
$0.request(.max(42))
|
||||
downstreamSubscription = $0
|
||||
})
|
||||
|
||||
ignoreOutput.subscribe(tracking)
|
||||
|
||||
XCTAssertNotNil(downstreamSubscription)
|
||||
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited)])
|
||||
|
||||
XCTAssertEqual(publisher.send(0), .none)
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited)])
|
||||
|
||||
XCTAssertEqual(publisher.send(2), .none)
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited)])
|
||||
|
||||
downstreamSubscription?.request(.max(95))
|
||||
downstreamSubscription?.request(.max(5))
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited)])
|
||||
|
||||
downstreamSubscription?.cancel()
|
||||
downstreamSubscription?.cancel()
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited),
|
||||
.cancelled])
|
||||
|
||||
downstreamSubscription?.request(.max(50))
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited),
|
||||
.cancelled])
|
||||
}
|
||||
|
||||
func testSendsSubcriptionDownstreamBeforeDemandUpstream() {
|
||||
let sentDemandRequestUpstream = "Sent demand request upstream"
|
||||
let sentSubscriptionDownstream = "Sent subcription downstream"
|
||||
var receiveOrder: [String] = []
|
||||
|
||||
let subscription = CustomSubscription(onRequest: { _ in
|
||||
receiveOrder.append(sentDemandRequestUpstream)
|
||||
})
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let ignoreOutputPublisher = publisher.ignoreOutput()
|
||||
let tracking =
|
||||
TrackingSubscriberBase<Never, TestingError>(receiveSubscription: { _ in
|
||||
receiveOrder.append(sentSubscriptionDownstream)
|
||||
})
|
||||
|
||||
ignoreOutputPublisher.subscribe(tracking)
|
||||
|
||||
XCTAssertEqual(receiveOrder, [sentSubscriptionDownstream,
|
||||
sentDemandRequestUpstream])
|
||||
}
|
||||
}
|
||||
@@ -16,57 +16,6 @@ import OpenCombine
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class JustTests: XCTestCase {
|
||||
|
||||
static let allTests = [
|
||||
("testJustNoInitialDemand", testJustNoInitialDemand),
|
||||
("testCustomMirror", testCustomMirror),
|
||||
("testJustWithInitialDemand", testJustWithInitialDemand),
|
||||
("testLifecycle", testLifecycle),
|
||||
("testCancelOnSubscription", testCancelOnSubscription),
|
||||
("testMinOperatorSpecialization", testMinOperatorSpecialization),
|
||||
("testMaxOperatorSpecialization", testMaxOperatorSpecialization),
|
||||
("testContainsOperatorSpecialization", testContainsOperatorSpecialization),
|
||||
("testTryContainsOperatorSpecialization", testTryContainsOperatorSpecialization),
|
||||
("testRemoveDuplicatesOperatorSpecialization",
|
||||
testRemoveDuplicatesOperatorSpecialization),
|
||||
("testTryRemoveDuplicatesOperatorSpecialization",
|
||||
testTryRemoveDuplicatesOperatorSpecialization),
|
||||
("testAllSatisfyOperatorSpecialization", testAllSatisfyOperatorSpecialization),
|
||||
("testTryAllSatisfyOperatorSpecialization",
|
||||
testTryAllSatisfyOperatorSpecialization),
|
||||
("testCollectOperatorSpecialization", testCollectOperatorSpecialization),
|
||||
("testCountOperatorSpecialization", testCountOperatorSpecialization),
|
||||
("testDropFirstOperatorSpecialization", testDropFirstOperatorSpecialization),
|
||||
("testDropWhileOperatorSpecialization", testDropWhileOperatorSpecialization),
|
||||
("testFirstOperatorSpecialization", testFirstOperatorSpecialization),
|
||||
("testFirstWhereOperatorSpecializtion", testFirstWhereOperatorSpecializtion),
|
||||
("testLastOperatorSpecialization", testLastOperatorSpecialization),
|
||||
("testLastWhereOperatorSpecializtion", testLastWhereOperatorSpecializtion),
|
||||
("testIgnoreOutputOperatorSpecialization",
|
||||
testIgnoreOutputOperatorSpecialization),
|
||||
("testMapOperatorSpecialization", testMapOperatorSpecialization),
|
||||
("testTryMapOperatorSpecialization", testTryMapOperatorSpecialization),
|
||||
("testCompactMapOperatorSpecialization", testCompactMapOperatorSpecialization),
|
||||
("testFilterOperatorSpecialization", testFilterOperatorSpecialization),
|
||||
("testMapErrorOperatorSpecialization", testMapErrorOperatorSpecialization),
|
||||
("testReplaceErrorOperatorSpecialization",
|
||||
testReplaceErrorOperatorSpecialization),
|
||||
("testReplaceEmptyOperatorSpecialization",
|
||||
testReplaceEmptyOperatorSpecialization),
|
||||
("testRetryOperatorSpecialization", testRetryOperatorSpecialization),
|
||||
("testReduceOperatorSpecialization", testReduceOperatorSpecialization),
|
||||
("testTryReduceOperatorSpecialization", testTryReduceOperatorSpecialization),
|
||||
("testScanOperatorSpecialization", testScanOperatorSpecialization),
|
||||
("testTryScanOperatorSpecialization", testTryScanOperatorSpecialization),
|
||||
("testOutputAtIndexOperatorSpecialization",
|
||||
testOutputAtIndexOperatorSpecialization),
|
||||
("testOutputInRangeOperatorSpecialization",
|
||||
testOutputInRangeOperatorSpecialization),
|
||||
("testPrefixOperatorSpecialization", testPrefixOperatorSpecialization),
|
||||
("testPrefixWhileOperatorSpecialization", testPrefixWhileOperatorSpecialization),
|
||||
("testSetFailureTypeOperatorSpecialization",
|
||||
testSetFailureTypeOperatorSpecialization),
|
||||
]
|
||||
|
||||
private typealias Sut = Just
|
||||
|
||||
func testJustNoInitialDemand() {
|
||||
|
||||
@@ -15,18 +15,6 @@ import OpenCombine
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class MapErrorTests: XCTestCase {
|
||||
static let allTests = [
|
||||
("testEmpty", testEmpty),
|
||||
("testError", testError),
|
||||
("testRange", testRange),
|
||||
("testNoDemand", testNoDemand),
|
||||
("testDemandSubscribe", testDemandSubscribe),
|
||||
("testDemandSend", testDemandSend),
|
||||
("testCompletion", testCompletion),
|
||||
("testCancel", testCancel),
|
||||
("testCancelAlreadyCancelled", testCancelAlreadyCancelled),
|
||||
("testLifecycle", testLifecycle),
|
||||
]
|
||||
|
||||
func testEmpty() {
|
||||
// Given
|
||||
|
||||
@@ -15,29 +15,6 @@ import OpenCombine
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class MapTests: XCTestCase {
|
||||
static let allTests = [
|
||||
("testEmpty", testEmpty),
|
||||
("testError", testError),
|
||||
("testTryMapFailureBecauseOfThrow", testTryMapFailureBecauseOfThrow),
|
||||
("testTryMapFailureOnCompletion", testTryMapFailureOnCompletion),
|
||||
("testTryMapSuccess", testTryMapSuccess),
|
||||
("testRange", testRange),
|
||||
("testNoDemand", testNoDemand),
|
||||
("testDemandSubscribe", testDemandSubscribe),
|
||||
("testDemandSend", testDemandSend),
|
||||
("testCompletion", testCompletion),
|
||||
("testMapCancel", testMapCancel),
|
||||
("testTryMapCancel", testTryMapCancel),
|
||||
("testCancelAlreadyCancelled", testCancelAlreadyCancelled),
|
||||
("testLifecycle", testLifecycle),
|
||||
("testMapOperatorSpecializationForMap", testMapOperatorSpecializationForMap),
|
||||
("testTryMapOperatorSpecializationForMap",
|
||||
testTryMapOperatorSpecializationForMap),
|
||||
("testMapOperatorSpecializationForTryMap",
|
||||
testMapOperatorSpecializationForTryMap),
|
||||
("testTryMapOperatorSpecializationForTryMap",
|
||||
testTryMapOperatorSpecializationForTryMap)
|
||||
]
|
||||
|
||||
func testEmpty() {
|
||||
// Given
|
||||
@@ -234,7 +211,7 @@ final class MapTests: XCTestCase {
|
||||
// When
|
||||
map.subscribe(tracking)
|
||||
try XCTUnwrap(downstreamSubscription).cancel()
|
||||
_ = publisher.send(1)
|
||||
XCTAssertEqual(publisher.send(1), .none)
|
||||
publisher.send(completion: .finished)
|
||||
// Then
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited), .cancelled])
|
||||
@@ -253,7 +230,7 @@ final class MapTests: XCTestCase {
|
||||
// When
|
||||
map.subscribe(tracking)
|
||||
try XCTUnwrap(downstreamSubscription).cancel()
|
||||
_ = publisher.send(1)
|
||||
XCTAssertEqual(publisher.send(1), .none)
|
||||
publisher.send(completion: .finished)
|
||||
// Then
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited), .cancelled])
|
||||
@@ -352,12 +329,14 @@ final class MapTests: XCTestCase {
|
||||
publisher.send(2)
|
||||
publisher.send(3)
|
||||
publisher.send(5)
|
||||
publisher.send(completion: .finished)
|
||||
|
||||
XCTAssert(map1.upstream === map2.upstream)
|
||||
XCTAssertEqual(tracking.history, [.subscription("PassthroughSubject"),
|
||||
.value(5),
|
||||
.value(7),
|
||||
.value(11)])
|
||||
.value(11),
|
||||
.completion(.finished)])
|
||||
}
|
||||
|
||||
func testTryMapOperatorSpecializationForMap() {
|
||||
|
||||
@@ -16,14 +16,6 @@ import OpenCombine
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class MulticastTests: XCTestCase {
|
||||
|
||||
static let allTests = [
|
||||
("testMulticast", testMulticast),
|
||||
("testMulticastConnectTwice", testMulticastConnectTwice),
|
||||
("testMulticastDisconnect", testMulticastDisconnect),
|
||||
("testLateSubscriber", testLateSubscriber),
|
||||
("testSubscribeAfterCompletion", testSubscribeAfterCompletion),
|
||||
]
|
||||
|
||||
func testMulticast() throws {
|
||||
|
||||
let publisher = CustomPublisher(subscription: CustomSubscription())
|
||||
|
||||
@@ -16,46 +16,6 @@ import OpenCombine
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class OptionalPublisherTests: XCTestCase {
|
||||
|
||||
static let allTests = [
|
||||
("testSuccessNoInitialDemand", testSuccessNoInitialDemand),
|
||||
("testSuccessWithInitialDemand", testSuccessWithInitialDemand),
|
||||
("testSuccessCancelOnSubscription", testSuccessCancelOnSubscription),
|
||||
("testNil", testNil),
|
||||
("testLifecycle", testLifecycle),
|
||||
("testMinOperatorSpecialization", testMinOperatorSpecialization),
|
||||
("testMaxOperatorSpecialization", testMaxOperatorSpecialization),
|
||||
("testContainsOperatorSpecialization", testContainsOperatorSpecialization),
|
||||
("testRemoveDuplicatesOperatorSpecialization",
|
||||
testRemoveDuplicatesOperatorSpecialization),
|
||||
("testAllSatifyOperatorSpecialization", testAllSatifyOperatorSpecialization),
|
||||
("testCollectOperatorSpecialization", testCollectOperatorSpecialization),
|
||||
("testCountOperatorSpecialization", testCountOperatorSpecialization),
|
||||
("testDropFirstOperatorSpecialization", testDropFirstOperatorSpecialization),
|
||||
("testDropWhileOperatorSpecialization", testDropWhileOperatorSpecialization),
|
||||
("testFirstOperatorSpecialization", testFirstOperatorSpecialization),
|
||||
("testFirstWhereOperatorSpecializtion", testFirstWhereOperatorSpecializtion),
|
||||
("testLastOperatorSpecialization", testLastOperatorSpecialization),
|
||||
("testLastWhereOperatorSpecializtion", testLastWhereOperatorSpecializtion),
|
||||
("testFilterOperatorSpecialization", testFilterOperatorSpecialization),
|
||||
("testIgnoreOutputOperatorSpecialization",
|
||||
testIgnoreOutputOperatorSpecialization),
|
||||
("testMapOperatorSpecialization", testMapOperatorSpecialization),
|
||||
("testCompactMapOperatorSpecialization", testCompactMapOperatorSpecialization),
|
||||
("testReplaceErrorOperatorSpecialization",
|
||||
testReplaceErrorOperatorSpecialization),
|
||||
("testReplaceEmptyOperatorSpecialization",
|
||||
testReplaceEmptyOperatorSpecialization),
|
||||
("testRetryOperatorSpecialization", testRetryOperatorSpecialization),
|
||||
("testReduceOperatorSpecialization", testReduceOperatorSpecialization),
|
||||
("testScanOperatorSpecialization", testScanOperatorSpecialization),
|
||||
("testOutputAtIndexOperatorSpecialization",
|
||||
testOutputAtIndexOperatorSpecialization),
|
||||
("testOutputInRangeOperatorSpecialization",
|
||||
testOutputInRangeOperatorSpecialization),
|
||||
("testPrefixOperatorSpecialization", testPrefixOperatorSpecialization),
|
||||
("testPrefixWhileOperatorSpecialization", testPrefixWhileOperatorSpecialization),
|
||||
]
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST || !canImport(Combine)
|
||||
private typealias Sut<Output> = Optional<Output>.Publisher
|
||||
#else
|
||||
@@ -194,7 +154,7 @@ final class OptionalPublisherTests: XCTestCase {
|
||||
|
||||
func testCollectOperatorSpecialization() {
|
||||
XCTAssertEqual(Sut<Int>(13).collect(), Sut([13]))
|
||||
XCTAssertEqual(Sut<Int>(nil).collect(), Sut(nil))
|
||||
XCTAssertEqual(Sut<Int>(nil).collect(), Sut([]))
|
||||
}
|
||||
|
||||
func testCountOperatorSpecialization() {
|
||||
|
||||
@@ -16,12 +16,6 @@ import OpenCombine
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class PrintTests: XCTestCase {
|
||||
|
||||
static let allTests = [
|
||||
("testPrintWithoutPrefix", testPrintWithoutPrefix),
|
||||
("testPrintWithPrefix", testPrintWithPrefix),
|
||||
("testSynchronization", testSynchronization),
|
||||
]
|
||||
|
||||
func testPrintWithoutPrefix() {
|
||||
|
||||
let stream = StringStream()
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
//
|
||||
// ReplaceErrorTests.swift
|
||||
// OpenCombineTests
|
||||
//
|
||||
// Created by Bogdan Vlad on 8/29/19.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class ReplaceErrorTests: XCTestCase {
|
||||
|
||||
func testEmpty() {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .none,
|
||||
createSut: { $0.replaceError(with: 42) })
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("ReplaceError")])
|
||||
}
|
||||
|
||||
func testError() {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(1),
|
||||
receiveValueDemand: .none,
|
||||
createSut: { $0.replaceError(with: 42) })
|
||||
|
||||
helper.publisher.send(completion: .failure(TestingError.oops))
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("ReplaceError"),
|
||||
.value(42),
|
||||
.completion(.finished)
|
||||
])
|
||||
}
|
||||
|
||||
func testWithoutError() {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(1),
|
||||
receiveValueDemand: .none,
|
||||
createSut: { $0.replaceError(with: 40) })
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(42), .none)
|
||||
helper.publisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("ReplaceError"),
|
||||
.value(42),
|
||||
.completion(.finished)])
|
||||
}
|
||||
|
||||
func testSendingValueAndThenError() {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(1),
|
||||
receiveValueDemand: .max(1),
|
||||
createSut: { $0.replaceError(with: 42) })
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(41), .max(1))
|
||||
helper.publisher.send(completion: .failure(TestingError.oops))
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("ReplaceError"),
|
||||
.value(41),
|
||||
.value(42),
|
||||
.completion(.finished)])
|
||||
}
|
||||
|
||||
func testFailingBeforeDemanding() {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .max(1),
|
||||
createSut: { $0.replaceError(with: 42) })
|
||||
|
||||
helper.publisher.send(completion: .failure(TestingError.oops))
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("ReplaceError")])
|
||||
|
||||
helper.downstreamSubscription?.request(.unlimited)
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("ReplaceError"),
|
||||
.value(42),
|
||||
.completion(.finished)])
|
||||
}
|
||||
|
||||
func testLifecycle() throws {
|
||||
var deinitCounter = 0
|
||||
|
||||
let onDeinit = { deinitCounter += 1 }
|
||||
|
||||
do {
|
||||
let passthrough = PassthroughSubject<Int, TestingError>()
|
||||
let replaceError = passthrough.replaceError(with: 10)
|
||||
let emptySubscriber = TrackingSubscriberBase<Int, Never>(
|
||||
onDeinit: onDeinit
|
||||
)
|
||||
XCTAssertTrue(emptySubscriber.history.isEmpty)
|
||||
replaceError.print("test").subscribe(emptySubscriber)
|
||||
XCTAssertEqual(emptySubscriber.subscriptions.count, 1)
|
||||
passthrough.send(31)
|
||||
XCTAssertEqual(emptySubscriber.inputs.count, 0)
|
||||
passthrough.send(completion: .failure("failure"))
|
||||
XCTAssertEqual(emptySubscriber.completions.count, 0)
|
||||
}
|
||||
|
||||
XCTAssertEqual(deinitCounter, 0)
|
||||
|
||||
do {
|
||||
let passthrough = PassthroughSubject<Int, TestingError>()
|
||||
let replaceError = passthrough.replaceError(with: 10)
|
||||
let emptySubscriber = TrackingSubscriberBase<Int, Never>(
|
||||
onDeinit: onDeinit
|
||||
)
|
||||
XCTAssertTrue(emptySubscriber.history.isEmpty)
|
||||
replaceError.subscribe(emptySubscriber)
|
||||
XCTAssertEqual(emptySubscriber.subscriptions.count, 1)
|
||||
XCTAssertEqual(emptySubscriber.inputs.count, 0)
|
||||
XCTAssertEqual(emptySubscriber.completions.count, 0)
|
||||
}
|
||||
|
||||
XCTAssertEqual(deinitCounter, 0)
|
||||
|
||||
var subscription: Subscription?
|
||||
|
||||
do {
|
||||
let passthrough = PassthroughSubject<Int, TestingError>()
|
||||
let replaceError = passthrough.replaceError(with: 10)
|
||||
let emptySubscriber = TrackingSubscriberBase<Int, Never>(
|
||||
receiveSubscription: { subscription = $0; $0.request(.unlimited) },
|
||||
onDeinit: onDeinit
|
||||
)
|
||||
XCTAssertTrue(emptySubscriber.history.isEmpty)
|
||||
replaceError.subscribe(emptySubscriber)
|
||||
XCTAssertEqual(emptySubscriber.subscriptions.count, 1)
|
||||
passthrough.send(31)
|
||||
XCTAssertEqual(emptySubscriber.inputs.count, 1)
|
||||
XCTAssertEqual(emptySubscriber.completions.count, 0)
|
||||
XCTAssertNotNil(subscription)
|
||||
}
|
||||
|
||||
XCTAssertEqual(deinitCounter, 0)
|
||||
try XCTUnwrap(subscription).cancel()
|
||||
XCTAssertEqual(deinitCounter, 0)
|
||||
}
|
||||
|
||||
func testCancelAlreadyCancelled() throws {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .max(1),
|
||||
createSut: { $0.replaceError(with: 42) })
|
||||
|
||||
helper.downstreamSubscription?.cancel()
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
helper.downstreamSubscription?.request(.unlimited)
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
|
||||
.cancelled])
|
||||
}
|
||||
|
||||
func testErrorWhileDownstreamDemandIsZero() {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(1),
|
||||
receiveValueDemand: .none,
|
||||
createSut: { $0.replaceError(with: 42) })
|
||||
|
||||
// Send demanded value
|
||||
_ = helper.publisher.send(9)
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("ReplaceError"),
|
||||
.value(9)])
|
||||
|
||||
helper.publisher.send(completion: .failure(TestingError.oops))
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("ReplaceError"),
|
||||
.value(9)])
|
||||
|
||||
helper.downstreamSubscription?.request(.max(1))
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("ReplaceError"),
|
||||
.value(9),
|
||||
.value(42),
|
||||
.completion(.finished)])
|
||||
}
|
||||
}
|
||||
|
||||
private struct OtherError: Error {
|
||||
let original: Error
|
||||
|
||||
init(_ original: Error) {
|
||||
self.original = original
|
||||
}
|
||||
}
|
||||
@@ -15,11 +15,6 @@ import OpenCombine
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class ReplaceNilTests: XCTestCase {
|
||||
static let allTests = [
|
||||
("testReplacesNilElement", testReplacesNilElement),
|
||||
("testExistingElementIsPreserved", testExistingElementIsPreserved),
|
||||
("testMultipleReplacements", testMultipleReplacements)
|
||||
]
|
||||
|
||||
func testReplacesNilElement() {
|
||||
// Given
|
||||
|
||||
@@ -16,49 +16,6 @@ import OpenCombine
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class ResultPublisherTests: XCTestCase {
|
||||
|
||||
static let allTests = [
|
||||
("testOnceSuccessNoInitialDemand", testOnceSuccessNoInitialDemand),
|
||||
("testOnceSuccessWithInitialDemand", testOnceSuccessWithInitialDemand),
|
||||
("testSuccessCancelOnSubscription", testSuccessCancelOnSubscription),
|
||||
("testOnceFailure", testOnceFailure),
|
||||
("testFailureCancelOnSubscription", testFailureCancelOnSubscription),
|
||||
("testLifecycle", testLifecycle),
|
||||
("testCustomMirror", testCustomMirror),
|
||||
("testMinOperatorSpecialization", testMinOperatorSpecialization),
|
||||
("testTryMinOperatorSpecialization", testTryMinOperatorSpecialization),
|
||||
("testMaxOperatorSpecialization", testMaxOperatorSpecialization),
|
||||
("testTryMaxOperatorSpecialization", testTryMaxOperatorSpecialization),
|
||||
("testContainsOperatorSpecialization", testContainsOperatorSpecialization),
|
||||
("testTryContainsOperatorSpecialization", testTryContainsOperatorSpecialization),
|
||||
("testRemoveDuplicatesOperatorSpecialization",
|
||||
testRemoveDuplicatesOperatorSpecialization),
|
||||
("testTryRemoveDuplicatesOperatorSpecialization",
|
||||
testTryRemoveDuplicatesOperatorSpecialization),
|
||||
("testAllSatifyOperatorSpecialization", testAllSatifyOperatorSpecialization),
|
||||
("testTryAllSatifyOperatorSpecialization",
|
||||
testTryAllSatifyOperatorSpecialization),
|
||||
("testCollectOperatorSpecialization", testCollectOperatorSpecialization),
|
||||
("testCountOperatorSpecialization", testCountOperatorSpecialization),
|
||||
("testFirstOperatorSpecialization", testFirstOperatorSpecialization),
|
||||
("testLastOperatorSpecialization", testLastOperatorSpecialization),
|
||||
("testIgnoreOutputOperatorSpecialization",
|
||||
testIgnoreOutputOperatorSpecialization),
|
||||
("testMapOperatorSpecialization", testMapOperatorSpecialization),
|
||||
("testTryMapOperatorSpecialization", testTryMapOperatorSpecialization),
|
||||
("testMapErrorOperatorSpecialization", testMapErrorOperatorSpecialization),
|
||||
("testReplaceErrorOperatorSpecialization",
|
||||
testReplaceErrorOperatorSpecialization),
|
||||
("testReplaceEmptyOperatorSpecialization",
|
||||
testReplaceEmptyOperatorSpecialization),
|
||||
("testRetryOperatorSpecialization", testRetryOperatorSpecialization),
|
||||
("testReduceOperatorSpecialization", testReduceOperatorSpecialization),
|
||||
("testTryReduceOperatorSpecialization", testTryReduceOperatorSpecialization),
|
||||
("testScanOperatorSpecialization", testScanOperatorSpecialization),
|
||||
("testTryScanOperatorSpecialization", testTryScanOperatorSpecialization),
|
||||
("testSetFailureTypeOperatorSpecialization",
|
||||
testSetFailureTypeOperatorSpecialization),
|
||||
]
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST || !canImport(Combine)
|
||||
private typealias ResultPublisher<Output, Failure: Error> =
|
||||
Result<Output, Failure>.Publisher
|
||||
|
||||
@@ -16,60 +16,6 @@ import OpenCombine
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class SequenceTests: XCTestCase {
|
||||
|
||||
static let allTests = [
|
||||
("testEmptySequence", testEmptySequence),
|
||||
("testSequenceNoInitialDemand", testSequenceNoInitialDemand),
|
||||
("testSequenceInitialDemand", testSequenceInitialDemand),
|
||||
("testCancelOnSubscription", testCancelOnSubscription),
|
||||
("testLifecycle", testLifecycle),
|
||||
("testAllSatisfyOperatorSpecialization", testAllSatisfyOperatorSpecialization),
|
||||
("testTryAllSatisfyOperatorSpecialization",
|
||||
testTryAllSatisfyOperatorSpecialization),
|
||||
("testCollectOperatorSpecialization", testCollectOperatorSpecialization),
|
||||
("testCompactMapOperatorSpecialization", testCompactMapOperatorSpecialization),
|
||||
("testMinOperatorSpecialization", testMinOperatorSpecialization),
|
||||
("testMaxOperatorSpecialization", testMaxOperatorSpecialization),
|
||||
("testContainsOperatorSpecialization", testContainsOperatorSpecialization),
|
||||
("testTryContainsOperatorSpecialization", testTryContainsOperatorSpecialization),
|
||||
("testDropWhileOperatorSpecialization", testDropWhileOperatorSpecialization),
|
||||
("testDropFirstOperatorSpecialization", testDropFirstOperatorSpecialization),
|
||||
("testFirstWhereOperatorSpecializtion", testFirstWhereOperatorSpecializtion),
|
||||
("testFilterOperatorSpecialization", testFilterOperatorSpecialization),
|
||||
("testIgnoreOutputOperatorSpecialization",
|
||||
testIgnoreOutputOperatorSpecialization),
|
||||
("testMapOperatorSpecialization", testMapOperatorSpecialization),
|
||||
("testPrefixOperatorSpecialization", testPrefixOperatorSpecialization),
|
||||
("testPrefixWhileOperatorSpecialization", testPrefixWhileOperatorSpecialization),
|
||||
("testReduceOperatorSpecialization", testReduceOperatorSpecialization),
|
||||
("testTryReduceOperatorSpecialization", testTryReduceOperatorSpecialization),
|
||||
("testReplaceNilOperatorSpecialization", testReplaceNilOperatorSpecialization),
|
||||
("testScanOperatorSpecialization", testScanOperatorSpecialization),
|
||||
("testSetFailureTypeOperatorSpecialization",
|
||||
testSetFailureTypeOperatorSpecialization),
|
||||
("testRemoveDuplicatesOperatorSpecialization",
|
||||
testRemoveDuplicatesOperatorSpecialization),
|
||||
("testFirstOperatorSpecialization", testFirstOperatorSpecialization),
|
||||
("testCountOperatorSpecialization", testCountOperatorSpecialization),
|
||||
("testOutputAtIndexOperatorSpecialization",
|
||||
testOutputAtIndexOperatorSpecialization),
|
||||
("testOutputInRangeOperatorSpecialization",
|
||||
testOutputInRangeOperatorSpecialization),
|
||||
("testLastOperatorSpecialization", testLastOperatorSpecialization),
|
||||
("testLastWhereOperatorSpecializtion", testLastWhereOperatorSpecializtion),
|
||||
("testPrependVariadicOperatorSpezialization",
|
||||
testPrependVariadicOperatorSpezialization),
|
||||
("testPrependSequenceOperatorSpecialization",
|
||||
testPrependSequenceOperatorSpecialization),
|
||||
("testPrependPublisherOperatorSpecialization",
|
||||
testPrependPublisherOperatorSpecialization),
|
||||
("testAppendVariadicOperatorSpezialization",
|
||||
testAppendVariadicOperatorSpezialization),
|
||||
("testAppendSequenceOperatorSpecialization",
|
||||
testAppendSequenceOperatorSpecialization),
|
||||
("testAppendPublisherOperatorSpecialization",
|
||||
testAppendPublisherOperatorSpecialization),
|
||||
]
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST || !canImport(Combine)
|
||||
private typealias ResultPublisher<Output, Failure: Error> =
|
||||
Result<Output, Failure>.Publisher
|
||||
|
||||
@@ -16,17 +16,6 @@ import OpenCombine
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class SetFailureTypeTests: XCTestCase {
|
||||
|
||||
static let allTests = [
|
||||
("testEmpty", testEmpty),
|
||||
("testForwardingValues", testForwardingValues),
|
||||
("testNoDemand", testNoDemand),
|
||||
("testDemandSubscribe", testDemandSubscribe),
|
||||
("testDemandSend", testDemandSend),
|
||||
("testCompletion", testCompletion),
|
||||
("testCancel", testCancel),
|
||||
("testCancelAlreadyCancelled", testCancelAlreadyCancelled),
|
||||
]
|
||||
|
||||
func testEmpty() {
|
||||
let tracking = TrackingSubscriberBase<Int, TestingError>(
|
||||
receiveSubscription: { $0.request(.unlimited) }
|
||||
|
||||
@@ -16,14 +16,6 @@ import OpenCombine
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class AssignTests: XCTestCase {
|
||||
|
||||
static let allTests = [
|
||||
("testDescription", testDescription),
|
||||
("testReflection", testReflection),
|
||||
("testSubscription", testSubscription),
|
||||
("testReceiveValue", testReceiveValue),
|
||||
("testPublisherOperator", testPublisherOperator),
|
||||
]
|
||||
|
||||
private typealias Sut<Root> = Subscribers.Assign<Root, Int>
|
||||
|
||||
private final class TestObject {
|
||||
@@ -58,7 +50,7 @@ final class AssignTests: XCTestCase {
|
||||
XCTAssertEqual(children[1].value as? ReferenceWritableKeyPath<TestObject, Int>,
|
||||
\.value)
|
||||
|
||||
XCTAssertEqual(children[2].label, "upstreamSubscription")
|
||||
XCTAssertEqual(children[2].label, "status")
|
||||
XCTAssertNotNil(children[2].value)
|
||||
}
|
||||
|
||||
|
||||
@@ -18,11 +18,7 @@ import Foundation
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class CompletionTests: XCTestCase {
|
||||
|
||||
static let allTests = [
|
||||
("testEncodingDecoding", testEncodingDecoding)
|
||||
]
|
||||
|
||||
typealias Sut = Subscribers.Completion<TestingError>
|
||||
private typealias Sut = Subscribers.Completion<TestingError>
|
||||
|
||||
let encoder: JSONEncoder = {
|
||||
let encoder = JSONEncoder()
|
||||
|
||||
@@ -16,14 +16,6 @@ import OpenCombine
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class SinkTests: XCTestCase {
|
||||
|
||||
static let allTests = [
|
||||
("testDescription", testDescription),
|
||||
("testReflection", testReflection),
|
||||
("testSubscription", testSubscription),
|
||||
("testReceiveValue", testReceiveValue),
|
||||
("testPublisherOperator", testPublisherOperator),
|
||||
]
|
||||
|
||||
private typealias Sut = Subscribers.Sink<Int, Never>
|
||||
|
||||
func testDescription() {
|
||||
|
||||
@@ -18,18 +18,6 @@ import OpenCombine
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class SubscribersDemandTests: XCTestCase {
|
||||
|
||||
static let allTests = [
|
||||
("testCrashesOnNegativeValue", testCrashesOnNegativeValue),
|
||||
("testAddition", testAddition),
|
||||
("testSubtraction", testSubtraction),
|
||||
("testMultiplication", testMultiplication),
|
||||
("testComparison", testComparison),
|
||||
("testMax", testMax),
|
||||
("testDescription", testDescription),
|
||||
("testEncodeDecodeJSON", testEncodeDecodeJSON),
|
||||
("testEncodeDecodePlist", testEncodeDecodePlist),
|
||||
]
|
||||
|
||||
func testCrashesOnNegativeValue() {
|
||||
assertCrashes {
|
||||
_ = Subscribers.Demand.max(-1)
|
||||
@@ -108,6 +96,7 @@ final class SubscribersDemandTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testComparison() {
|
||||
// swiftlint:disable no_space_in_method_call
|
||||
XCTAssertFalse(Subscribers.Demand.unlimited < .unlimited)
|
||||
XCTAssertFalse(Subscribers.Demand.unlimited > .unlimited)
|
||||
XCTAssertFalse(Subscribers.Demand.unlimited != .unlimited)
|
||||
@@ -197,6 +186,7 @@ final class SubscribersDemandTests: XCTestCase {
|
||||
XCTAssertTrue (100 == Subscribers.Demand.max(100))
|
||||
XCTAssertTrue (100 <= Subscribers.Demand.max(100))
|
||||
XCTAssertTrue (100 >= Subscribers.Demand.max(100))
|
||||
// swiftlint:enable no_space_in_method_call
|
||||
}
|
||||
|
||||
func testMax() {
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
//
|
||||
// Subscribers.Demand.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 10.06.2019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if !canImport(ObjectiveC)
|
||||
public func allTests() -> [XCTestCaseEntry] {
|
||||
return [
|
||||
testCase(AnyCancellableTests.allTests),
|
||||
testCase(AnyPublisherTests.allTests),
|
||||
testCase(AssignTests.allTests),
|
||||
testCase(CombineIdentifierTests.allTests),
|
||||
testCase(CompletionTests.allTests),
|
||||
testCase(CurrentValueSubjectTests.allTests),
|
||||
testCase(DecodeTests.allTests),
|
||||
testCase(DropWhileTests.allTests),
|
||||
testCase(EmptyTests.allTests),
|
||||
testCase(EncodeTests.allTests),
|
||||
testCase(FailTests.allTests),
|
||||
testCase(ImmediateSchedulerTests.allTests),
|
||||
testCase(JustTests.allTests),
|
||||
testCase(MapErrorTests.allTests),
|
||||
testCase(MapTests.allTests),
|
||||
testCase(MulticastTests.allTests),
|
||||
testCase(ResultPublisherTests.allTests),
|
||||
testCase(OptionalPublisherTests.allTests),
|
||||
testCase(PassthroughSubjectTests.allTests),
|
||||
testCase(PrintTests.allTests),
|
||||
testCase(PublisherTests.allTests),
|
||||
testCase(ReplaceNilTests.allTests),
|
||||
testCase(SetFailureTypeTests.allTests),
|
||||
testCase(SequenceTests.allTests),
|
||||
testCase(SinkTests.allTests),
|
||||
testCase(SubscribersDemandTests.allTests),
|
||||
]
|
||||
}
|
||||
#endif
|
||||
Reference in New Issue
Block a user