Compare commits
69 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5f92ee05d2 | |||
| bdd703abb3 | |||
| e41c48a5cd | |||
| df0b8b08db | |||
| 7056143b99 | |||
| 0a965ba60a | |||
| 7dfaa4edea | |||
| 3e8f2774a4 | |||
| 68e9bbe164 | |||
| 0f71c33d72 | |||
| 3f61648f82 | |||
| c621ceb267 | |||
| 2aa297ec39 | |||
| 9cb27bb91b | |||
| d74f68da86 | |||
| f68dcd520f | |||
| 432fd4f48f | |||
| 9c6bbda0c4 | |||
| 3990ec2afb | |||
| 39dd9e40bf | |||
| fd7c0459b9 | |||
| f7145e7fa5 | |||
| ecd4766129 | |||
| e00a6f06fc | |||
| 23ee3a4b7b | |||
| 9c913124eb | |||
| 7ddd15b334 | |||
| 72753ef93c | |||
| 816426b48c | |||
| 47fb390081 | |||
| 1d3327f6bf | |||
| eb7478d430 | |||
| f69621f0e2 | |||
| 7f3cccf1ae | |||
| ec037dbb3d | |||
| 8a39f35d3f | |||
| 7fb92bffc6 | |||
| e441ea3048 | |||
| 22f7b6d10d | |||
| 7431d21c9c | |||
| 1d901fca7f | |||
| 9834eab0ea | |||
| 1ce9660ce9 | |||
| 313d6befa6 | |||
| 8c7f061892 | |||
| 2ac2470579 | |||
| 57c9ae8590 | |||
| d57c878651 | |||
| 7fa91778c2 | |||
| d15e604764 | |||
| 07c7a98d72 | |||
| 01ef05be1f | |||
| beee9d0d51 | |||
| aacd1a326c | |||
| 5528adcc67 | |||
| 1b810d0536 | |||
| 8b25238154 | |||
| 9b9915bde7 | |||
| 27f01e5f21 | |||
| 739eb47409 | |||
| 14d5a90e89 | |||
| 0e869bc861 | |||
| 2f38069166 | |||
| 97d07d0a14 | |||
| d3888a3808 | |||
| d2b8709afb | |||
| a28177e9c5 | |||
| cef19fce4b | |||
| 7f6bba62de |
@@ -2,6 +2,7 @@
|
||||
/.build
|
||||
/Packages
|
||||
/*.xcodeproj
|
||||
/.swiftpm
|
||||
|
||||
# Created by https://www.gitignore.io/api/Xcode
|
||||
# Edit at https://www.gitignore.io/?templates=Xcode
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,102 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1100"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "OpenCombine"
|
||||
BuildableName = "OpenCombine"
|
||||
BlueprintName = "OpenCombine"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "OpenCombineTests"
|
||||
BuildableName = "OpenCombineTests"
|
||||
BlueprintName = "OpenCombineTests"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
enableThreadSanitizer = "YES"
|
||||
codeCoverageEnabled = "YES">
|
||||
<CodeCoverageTargets>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "OpenCombine"
|
||||
BuildableName = "OpenCombine"
|
||||
BlueprintName = "OpenCombine"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</CodeCoverageTargets>
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "OpenCombineTests"
|
||||
BuildableName = "OpenCombineTests"
|
||||
BlueprintName = "OpenCombineTests"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "OpenCombine"
|
||||
BuildableName = "OpenCombine"
|
||||
BlueprintName = "OpenCombine"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
+44
-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,43 @@ 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-debug-sanitize-thread;
|
||||
else
|
||||
make test-debug-sanitize-thread;
|
||||
fi
|
||||
fi
|
||||
- if [[ $OPENCOMBINE_TEST == "YES" ]]; then
|
||||
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,41 @@
|
||||
SWIFT_TEST_FLAGS=
|
||||
|
||||
debug:
|
||||
swift build -c debug
|
||||
|
||||
release:
|
||||
swift build -c release
|
||||
|
||||
test-debug:
|
||||
swift test -c debug $(SWIFT_TEST_FLAGS)
|
||||
|
||||
test-debug-sanitize-thread:
|
||||
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
|
||||
+10
-3
@@ -6,13 +6,20 @@ let package = Package(
|
||||
name: "OpenCombine",
|
||||
products: [
|
||||
.library(name: "OpenCombine", targets: ["OpenCombine"]),
|
||||
.library(name: "OpenCombineDispatch", targets: ["OpenCombineDispatch"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/broadwaylamb/GottaGoFast.git", from: "0.1.0")
|
||||
],
|
||||
targets: [
|
||||
.target(name: "OpenCombine"),
|
||||
.target(name: "COpenCombineHelpers"),
|
||||
.target(name: "OpenCombine", dependencies: ["COpenCombineHelpers"]),
|
||||
.target(name: "OpenCombineDispatch", dependencies: ["OpenCombine"]),
|
||||
.testTarget(name: "OpenCombineTests",
|
||||
dependencies: ["OpenCombine", "GottaGoFast"])
|
||||
]
|
||||
dependencies: ["OpenCombine",
|
||||
"OpenCombineDispatch",
|
||||
"GottaGoFast"],
|
||||
swiftSettings: [.unsafeFlags(["-enable-testing"])])
|
||||
],
|
||||
cxxLanguageStandard: .cxx1z
|
||||
)
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -1,22 +1,7 @@
|
||||
// This file contains parts of Apple's Combine that remain unimplemented in OpenCombine
|
||||
// Please remove the corresponding piece from this file if you implment something,
|
||||
// Please remove the corresponding piece from this file if you implement something,
|
||||
// and complement this file as features are added in Apple's Combine
|
||||
|
||||
extension ConnectablePublisher {
|
||||
|
||||
/// Automates the process of connecting or disconnecting from this connectable publisher.
|
||||
///
|
||||
/// Use `autoconnect()` to simplify working with `ConnectablePublisher` instances, such as those created with `makeConnectable()`.
|
||||
///
|
||||
/// let autoconnectedPublisher = somePublisher
|
||||
/// .makeConnectable()
|
||||
/// .autoconnect()
|
||||
/// .subscribe(someSubscriber)
|
||||
///
|
||||
/// - Returns: A publisher which automatically connects to its upstream connectable publisher.
|
||||
public func autoconnect() -> Publishers.Autoconnect<Self>
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that receives elements from an upstream publisher on a specific scheduler.
|
||||
@@ -115,82 +100,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.
|
||||
@@ -612,34 +521,6 @@ extension Publisher {
|
||||
public func combineLatest<P, Q, R, T>(_ publisher1: P, _ publisher2: Q, _ publisher3: R, _ transform: @escaping (Self.Output, P.Output, Q.Output, R.Output) -> T) -> Publishers.Map<Publishers.CombineLatest4<Self, P, Q, R>, T> where P : Publisher, Q : Publisher, R : Publisher, Self.Failure == P.Failure, P.Failure == Q.Failure, Q.Failure == R.Failure
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that automatically connects and disconnects from this connectable publisher.
|
||||
public class Autoconnect<Upstream> : Publisher where Upstream : ConnectablePublisher {
|
||||
|
||||
/// 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.
|
||||
final 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
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that republishes elements while a predicate closure indicates publishing should continue.
|
||||
@@ -819,43 +700,6 @@ extension Publisher {
|
||||
public func tryContains(where predicate: @escaping (Self.Output) throws -> Bool) -> Publishers.TryContainsWhere<Self>
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
public struct MakeConnectable<Upstream> : ConnectablePublisher 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
|
||||
|
||||
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
|
||||
|
||||
/// Connects to the publisher and returns a `Cancellable` instance with which to cancel publishing.
|
||||
///
|
||||
/// - Returns: A `Cancellable` instance that can be used to cancel publishing.
|
||||
public func connect() -> Cancellable
|
||||
}
|
||||
}
|
||||
|
||||
extension Publisher where Self.Failure == Never {
|
||||
|
||||
/// Creates a connectable wrapper around the publisher.
|
||||
///
|
||||
/// - Returns: A `ConnectablePublisher` wrapping this publisher.
|
||||
public func makeConnectable() -> Publishers.MakeConnectable<Self>
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A strategy for collecting received elements.
|
||||
@@ -1164,77 +1008,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 +1550,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.
|
||||
@@ -1966,53 +1702,6 @@ extension Publisher {
|
||||
public func throttle<S>(for interval: S.SchedulerTimeType.Stride, scheduler: S, latest: Bool) -> Publishers.Throttle<Self, S> where S : Scheduler
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher implemented as a class, which otherwise behaves like its upstream publisher.
|
||||
final public class Share<Upstream> : Publisher, Equatable 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
|
||||
|
||||
final 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.
|
||||
final public func receive<S>(subscriber: S) where S : Subscriber, Upstream.Failure == S.Failure, Upstream.Output == S.Input
|
||||
|
||||
/// Returns a Boolean value indicating whether two values are equal.
|
||||
///
|
||||
/// Equality is the inverse of inequality. For any values `a` and `b`,
|
||||
/// `a == b` implies that `a != b` is `false`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - lhs: A value to compare.
|
||||
/// - rhs: Another value to compare.
|
||||
public static func == (lhs: Publishers.Share<Upstream>, rhs: Publishers.Share<Upstream>) -> Bool
|
||||
}
|
||||
}
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Returns a publisher as a class instance.
|
||||
///
|
||||
/// The downstream subscriber receieves elements and completion states unchanged from the upstream publisher. Use this operator when you want to use reference semantics, such as storing a publisher instance in a property.
|
||||
///
|
||||
/// - Returns: A class instance that republishes its upstream publisher.
|
||||
public func share() -> Publishers.Share<Self>
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that republishes items from another publisher only if each new item is in increasing order from the previously-published item.
|
||||
@@ -2147,45 +1836,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 +2606,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 +2698,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 +2783,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 +2886,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.
|
||||
@@ -3445,17 +2910,6 @@ extension Publishers.ReplaceEmpty : Equatable where Upstream : Equatable, Upstre
|
||||
public static func == (lhs: Publishers.ReplaceEmpty<Upstream>, rhs: Publishers.ReplaceEmpty<Upstream>) -> Bool
|
||||
}
|
||||
|
||||
extension Publishers.ReplaceError : Equatable where Upstream : Equatable, Upstream.Output : Equatable {
|
||||
|
||||
/// Returns a Boolean value that indicates whether two publishers are equivalent.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - lhs: A replace error publisher to compare for equality.
|
||||
/// - rhs: Another replace error publisher to compare for equality.
|
||||
/// - Returns: `true` if the two publishers have equal upstream publishers and output elements, `false` otherwise.
|
||||
public static func == (lhs: Publishers.ReplaceError<Upstream>, rhs: Publishers.ReplaceError<Upstream>) -> Bool
|
||||
}
|
||||
|
||||
extension Publishers.DropUntilOutput : Equatable where Upstream : Equatable, Other : Equatable {
|
||||
|
||||
/// Returns a Boolean value indicating whether two values are equal.
|
||||
@@ -3564,51 +3018,6 @@ extension Publishers.Drop : Equatable where Upstream : Equatable {
|
||||
public static func == (lhs: Publishers.Drop<Upstream>, rhs: Publishers.Drop<Upstream>) -> Bool
|
||||
}
|
||||
|
||||
extension Publishers.First : Equatable where Upstream : Equatable {
|
||||
|
||||
/// Returns a Boolean value that indicates whether two first publishers have equal upstream publishers.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - lhs: A drop publisher to compare for equality.
|
||||
/// - rhs: Another drop publisher to compare for equality.
|
||||
/// - Returns: `true` if the two publishers have equal upstream publishers, `false` otherwise.
|
||||
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`
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
//
|
||||
// COpenCombineHelpers.cpp
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 23/09/2019.
|
||||
//
|
||||
|
||||
#include "COpenCombineHelpers.h"
|
||||
|
||||
#include <atomic>
|
||||
|
||||
extern "C" uint64_t opencombine_next_combine_identifier() {
|
||||
static std::atomic<uint64_t> next_combine_identifier;
|
||||
return next_combine_identifier.fetch_add(1);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// COpenCombineHelpers.h
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 23/09/2019.
|
||||
//
|
||||
|
||||
#ifndef COPENCOMBINEHELPERS_H
|
||||
#define COPENCOMBINEHELPERS_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
uint64_t opencombine_next_combine_identifier();
|
||||
|
||||
#ifdef __cplusplus
|
||||
} // extern "C"
|
||||
#endif
|
||||
|
||||
#endif /* COPENCOMBINEHELPERS_H */
|
||||
@@ -5,6 +5,12 @@
|
||||
// Created by Sergej Jaskiewicz on 10.06.2019.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
public func eraseToAnyPublisher() -> AnyPublisher<Output, Failure> {
|
||||
return .init(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// A type-erasing publisher.
|
||||
///
|
||||
/// Use `AnyPublisher` to wrap a publisher whose type has details you don’t want to expose
|
||||
@@ -13,7 +19,6 @@ public struct AnyPublisher<Output, Failure: Error>
|
||||
: CustomStringConvertible,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
{
|
||||
|
||||
@usableFromInline
|
||||
internal let box: PublisherBoxBase<Output, Failure>
|
||||
|
||||
@@ -47,8 +52,8 @@ extension AnyPublisher: Publisher {
|
||||
/// - subscriber: The subscriber to attach to this `Publisher`.
|
||||
/// once attached it can begin to receive values.
|
||||
@inlinable
|
||||
public func receive<SubscriberType: Subscriber>(subscriber: SubscriberType)
|
||||
where Output == SubscriberType.Input, Failure == SubscriberType.Failure
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Output == Downstream.Input, Failure == Downstream.Failure
|
||||
{
|
||||
box.subscribe(subscriber)
|
||||
}
|
||||
@@ -62,11 +67,11 @@ internal class PublisherBoxBase<Output, Failure: Error>: Publisher {
|
||||
@inlinable
|
||||
internal init() {}
|
||||
|
||||
@inlinable
|
||||
internal func receive<SubscriberType: Subscriber>(subscriber: SubscriberType)
|
||||
where Failure == SubscriberType.Failure, Output == SubscriberType.Input
|
||||
@usableFromInline
|
||||
internal func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Failure == Downstream.Failure, Output == Downstream.Input
|
||||
{
|
||||
fatalError()
|
||||
abstractMethod()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,8 +89,8 @@ internal final class PublisherBox<PublisherType: Publisher>
|
||||
}
|
||||
|
||||
@inlinable
|
||||
override internal func receive<SubscriberType: Subscriber>(subscriber: SubscriberType)
|
||||
where Failure == SubscriberType.Failure, Output == SubscriberType.Input
|
||||
override internal func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Failure == Downstream.Failure, Output == Downstream.Input
|
||||
{
|
||||
base.subscribe(subscriber)
|
||||
}
|
||||
|
||||
@@ -137,17 +137,17 @@ internal class AnySubscriberBase<Input, Failure: Error>: Subscriber {
|
||||
|
||||
@usableFromInline
|
||||
internal func receive(subscription: Subscription) {
|
||||
fatalError()
|
||||
abstractMethod()
|
||||
}
|
||||
|
||||
@usableFromInline
|
||||
internal func receive(_ input: Input) -> Subscribers.Demand {
|
||||
fatalError()
|
||||
abstractMethod()
|
||||
}
|
||||
|
||||
@usableFromInline
|
||||
internal func receive(completion: Subscribers.Completion<Failure>) {
|
||||
fatalError()
|
||||
abstractMethod()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,52 +222,3 @@ internal final class ClosureBasedAnySubscriber<Input, Failure: Error>
|
||||
receiveCompletionThunk(completion)
|
||||
}
|
||||
}
|
||||
|
||||
internal final class SubjectSubscriber<Downstream: Subject>
|
||||
: Subscriber,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
Subscription
|
||||
{
|
||||
internal var downstreamSubject: Downstream?
|
||||
internal var upstreamSubscription: Subscription?
|
||||
|
||||
internal init(_ parent: Downstream) {
|
||||
self.downstreamSubject = parent
|
||||
}
|
||||
|
||||
internal func receive(subscription: Subscription) {
|
||||
guard upstreamSubscription == nil else { return }
|
||||
upstreamSubscription = subscription
|
||||
downstreamSubject?.send(subscription: self)
|
||||
}
|
||||
|
||||
internal func receive(_ input: Downstream.Output) -> Subscribers.Demand {
|
||||
downstreamSubject?.send(input)
|
||||
return .none
|
||||
}
|
||||
|
||||
internal func receive(completion: Subscribers.Completion<Downstream.Failure>) {
|
||||
downstreamSubject?.send(completion: completion)
|
||||
}
|
||||
|
||||
internal var description: String { return "Subject" }
|
||||
|
||||
internal var customMirror: Mirror {
|
||||
let children: [(label: String?, value: Any)] = [
|
||||
(label: "downstreamSubject", value: downstreamSubject as Any),
|
||||
(label: "upstreamSubscription", value: upstreamSubscription as Any)
|
||||
]
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
internal func request(_ demand: Subscribers.Demand) {
|
||||
upstreamSubscription?.request(demand)
|
||||
}
|
||||
|
||||
internal func cancel() {
|
||||
upstreamSubscription?.cancel()
|
||||
upstreamSubscription = nil
|
||||
downstreamSubject = nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,35 +5,21 @@
|
||||
// Created by Sergej Jaskiewicz on 10.06.2019.
|
||||
//
|
||||
|
||||
import func COpenCombineHelpers.opencombine_next_combine_identifier
|
||||
|
||||
public struct CombineIdentifier: Hashable, CustomStringConvertible {
|
||||
|
||||
@usableFromInline
|
||||
internal static var _counter: UInt = 0
|
||||
private let id: UInt64
|
||||
|
||||
@usableFromInline
|
||||
internal static var _counterLock = Lock(recursive: false)
|
||||
|
||||
@usableFromInline
|
||||
internal let _id: UInt
|
||||
|
||||
@inlinable
|
||||
public init() {
|
||||
|
||||
var id: UInt = 0
|
||||
|
||||
CombineIdentifier._counterLock.do {
|
||||
id = CombineIdentifier._counter
|
||||
CombineIdentifier._counter += 1
|
||||
}
|
||||
|
||||
_id = id
|
||||
self.id = opencombine_next_combine_identifier()
|
||||
}
|
||||
|
||||
public init(_ obj: AnyObject) {
|
||||
_id = UInt(bitPattern: ObjectIdentifier(obj))
|
||||
id = UInt64(UInt(bitPattern: ObjectIdentifier(obj)))
|
||||
}
|
||||
|
||||
public var description: String {
|
||||
return "0x\(String(_id, radix: 16))"
|
||||
return "0x\(String(id, radix: 16))"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,11 +9,13 @@
|
||||
/// changes.
|
||||
public final class CurrentValueSubject<Output, Failure: Error>: Subject {
|
||||
|
||||
private let _lock = Lock(recursive: true)
|
||||
private let _lock = unfairRecursiveLock()
|
||||
|
||||
// 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)
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
//
|
||||
// Locking.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 11.06.2019.
|
||||
//
|
||||
|
||||
#if canImport(Darwin)
|
||||
import Darwin
|
||||
#elseif canImport(Glibc)
|
||||
import Glibc
|
||||
#else
|
||||
#error("How to do locking on this platform?")
|
||||
#endif
|
||||
|
||||
@usableFromInline
|
||||
internal protocol UnfairLock: AnyObject {
|
||||
func lock()
|
||||
func unlock()
|
||||
}
|
||||
|
||||
extension UnfairLock {
|
||||
|
||||
@inlinable
|
||||
internal func `do`<Result>(_ body: () throws -> Result) rethrows -> Result {
|
||||
lock()
|
||||
defer { unlock() }
|
||||
return try body()
|
||||
}
|
||||
}
|
||||
|
||||
internal protocol UnfairRecursiveLock: UnfairLock {}
|
||||
|
||||
internal func unfairLock() -> UnfairLock {
|
||||
#if canImport(Darwin)
|
||||
if #available(macOS 10.12, iOS 10.0, tvOS 10.0, watchOS 3.0, *) {
|
||||
return OSUnfairLock()
|
||||
} else {
|
||||
return PThreadMutexLock()
|
||||
}
|
||||
#else
|
||||
return PThreadMutexLock()
|
||||
#endif
|
||||
}
|
||||
|
||||
internal func unfairRecursiveLock() -> UnfairRecursiveLock {
|
||||
// TODO: Use os_unfair_recursive_lock on Darwin as soon as it becomes public API.
|
||||
return PThreadMutexRecursiveLock()
|
||||
}
|
||||
|
||||
private class PThreadMutexLock
|
||||
: UnfairLock,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
{
|
||||
private let mutex = UnsafeMutablePointer<pthread_mutex_t>.allocate(capacity: 1)
|
||||
|
||||
init() {
|
||||
var status: CInt
|
||||
var attributes = pthread_mutexattr_t()
|
||||
status = pthread_mutexattr_init(&attributes)
|
||||
precondition(status == 0,
|
||||
"pthread_mutexattr_init returned non-zero status: \(status)")
|
||||
|
||||
// Enable error detection
|
||||
status = pthread_mutexattr_settype(&attributes, CInt(PTHREAD_MUTEX_ERRORCHECK))
|
||||
precondition(status == 0,
|
||||
"pthread_mutexattr_settype returned non-zero status: \(status)")
|
||||
|
||||
setAdditionalAttributes(&attributes)
|
||||
|
||||
status = pthread_mutex_init(mutex, &attributes)
|
||||
precondition(status == 0,
|
||||
"pthread_mutex_init returned non-zero status: \(status)")
|
||||
}
|
||||
|
||||
fileprivate func setAdditionalAttributes(
|
||||
_ attributes: UnsafeMutablePointer<pthread_mutexattr_t>
|
||||
) {
|
||||
// Do nothing for non-recursive locks
|
||||
}
|
||||
|
||||
final func lock() {
|
||||
let status = pthread_mutex_lock(mutex)
|
||||
precondition(status == 0,
|
||||
"pthread_mutex_lock returned non-zero status: \(status)")
|
||||
}
|
||||
|
||||
final func unlock() {
|
||||
let status = pthread_mutex_unlock(mutex)
|
||||
precondition(status == 0,
|
||||
"pthread_mutex_lock returned non-zero status: \(status)")
|
||||
}
|
||||
|
||||
final var description: String { return String(describing: mutex.pointee) }
|
||||
|
||||
final var customMirror: Mirror { return Mirror(reflecting: mutex.pointee) }
|
||||
|
||||
final var playgroundDescription: Any { return description }
|
||||
|
||||
deinit {
|
||||
let status = pthread_mutex_destroy(mutex)
|
||||
precondition(status == 0,
|
||||
"pthread_mutex_destroy returned non-zero status: \(status)")
|
||||
mutex.deallocate()
|
||||
}
|
||||
}
|
||||
|
||||
private final class PThreadMutexRecursiveLock: PThreadMutexLock, UnfairRecursiveLock {
|
||||
override func setAdditionalAttributes(
|
||||
_ attributes: UnsafeMutablePointer<pthread_mutexattr_t>
|
||||
) {
|
||||
let status = pthread_mutexattr_settype(attributes, CInt(PTHREAD_MUTEX_RECURSIVE))
|
||||
precondition(status == 0,
|
||||
"pthread_mutexattr_settype returned non-zero status: \(status)")
|
||||
}
|
||||
}
|
||||
|
||||
#if canImport(Darwin)
|
||||
|
||||
@available(macOS 10.12, iOS 10.0, tvOS 10.0, watchOS 3.0, *)
|
||||
private final class OSUnfairLock
|
||||
: UnfairLock,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
{
|
||||
private var mutex = os_unfair_lock()
|
||||
|
||||
func lock() {
|
||||
os_unfair_lock_lock(&mutex)
|
||||
}
|
||||
|
||||
func unlock() {
|
||||
os_unfair_lock_unlock(&mutex)
|
||||
}
|
||||
|
||||
var description: String { return String(describing: mutex) }
|
||||
|
||||
var customMirror: Mirror { return Mirror(reflecting: mutex) }
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
|
||||
#endif // canImport(Darwin)
|
||||
@@ -0,0 +1,99 @@
|
||||
//
|
||||
// SubjectSubscriber.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 16/09/2019.
|
||||
//
|
||||
|
||||
// NOTE: This class has been audited for thread safety.
|
||||
internal final class SubjectSubscriber<Downstream: Subject>
|
||||
: Subscriber,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible,
|
||||
Subscription
|
||||
{
|
||||
private let lock = unfairLock()
|
||||
private var downstreamSubject: Downstream?
|
||||
private var upstreamSubscription: Subscription?
|
||||
|
||||
private var isCancelled: Bool { return downstreamSubject == nil }
|
||||
|
||||
internal init(_ parent: Downstream) {
|
||||
self.downstreamSubject = parent
|
||||
}
|
||||
|
||||
internal func receive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard upstreamSubscription == nil, let subject = downstreamSubject else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
upstreamSubscription = subscription
|
||||
lock.unlock()
|
||||
subject.send(subscription: self)
|
||||
}
|
||||
|
||||
internal func receive(_ input: Downstream.Output) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
guard let downstreamSubject = downstreamSubject else {
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
guard upstreamSubscription != nil else { APIViolationValueBeforeSubscription() }
|
||||
lock.unlock()
|
||||
downstreamSubject.send(input)
|
||||
return .none
|
||||
}
|
||||
|
||||
internal func receive(completion: Subscribers.Completion<Downstream.Failure>) {
|
||||
lock.lock()
|
||||
guard let subject = downstreamSubject else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
guard upstreamSubscription != nil else { APIViolationUnexpectedCompletion() }
|
||||
lock.unlock()
|
||||
subject.send(completion: completion)
|
||||
downstreamSubject = nil
|
||||
}
|
||||
|
||||
internal var description: String { return "Subject" }
|
||||
|
||||
internal var playgroundDescription: Any { return description }
|
||||
|
||||
internal var customMirror: Mirror {
|
||||
let children: [Mirror.Child] = [
|
||||
("downstreamSubject", downstreamSubject as Any),
|
||||
("upstreamSubscription", upstreamSubscription as Any),
|
||||
("subject", downstreamSubject as Any)
|
||||
]
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
internal func request(_ demand: Subscribers.Demand) {
|
||||
lock.lock()
|
||||
guard let subscription = upstreamSubscription else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
lock.unlock()
|
||||
subscription.request(demand)
|
||||
}
|
||||
|
||||
internal func cancel() {
|
||||
lock.lock()
|
||||
if isCancelled {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
guard let subscription = upstreamSubscription else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
upstreamSubscription = nil
|
||||
downstreamSubject = nil
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
//
|
||||
// SubscriptionStatus.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 21.09.2019.
|
||||
//
|
||||
|
||||
internal enum SubscriptionStatus {
|
||||
case awaitingSubscription
|
||||
case subscribed(Subscription)
|
||||
case terminal
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
//
|
||||
// Unreachable.swift
|
||||
// Unreachable
|
||||
//
|
||||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (c) 2017 Nikolai Vazquez
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
// All the credits go to https://github.com/nvzqz/Unreachable
|
||||
|
||||
/// An unreachable code path.
|
||||
///
|
||||
/// This can be used for whenever the compiler can't determine that a
|
||||
/// path is unreachable, such as dynamically terminating an iterator.
|
||||
@inline(__always)
|
||||
private func unsafeUnreachable() -> Never {
|
||||
return unsafeBitCast((), to: Never.self)
|
||||
}
|
||||
|
||||
/// Asserts that the code path is unreachable.
|
||||
///
|
||||
/// Calls `assertionFailure(_:file:line:)` in unoptimized builds and `unreachable()`
|
||||
/// otherwise.
|
||||
///
|
||||
/// - parameter message: The message to print. The default is
|
||||
/// "Encountered unreachable path".
|
||||
/// - parameter file: The file name to print with the message. The default is the file
|
||||
/// where this function is called.
|
||||
/// - parameter line: The line number to print with the message. The default is the line
|
||||
/// where this function is called.
|
||||
@inline(__always)
|
||||
internal func unreachable(
|
||||
_ message: @autoclosure () -> String = "Encountered unreachable path",
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line
|
||||
) -> Never {
|
||||
var isDebug = false
|
||||
assert({ isDebug = true; return true }())
|
||||
if isDebug {
|
||||
fatalError(message(), file: file, line: line)
|
||||
} else {
|
||||
unsafeUnreachable()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
//
|
||||
// Violations.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 16/09/2019.
|
||||
//
|
||||
|
||||
internal func APIViolationValueBeforeSubscription(file: StaticString = #file,
|
||||
line: UInt = #line) -> Never {
|
||||
fatalError("""
|
||||
API Violation: received an unexpected value before receiving a Subscription
|
||||
""",
|
||||
file: file,
|
||||
line: line)
|
||||
}
|
||||
|
||||
internal func APIViolationUnexpectedCompletion(file: StaticString = #file,
|
||||
line: UInt = #line) -> Never {
|
||||
fatalError("API Violation: received an unexpected completion", file: file, line: line)
|
||||
}
|
||||
|
||||
@inline(__always)
|
||||
internal func abstractMethod(file: StaticString = #file, line: UInt = #line) -> Never {
|
||||
unreachable("Abstract method call", file: file, line: line)
|
||||
}
|
||||
|
||||
extension Subscribers.Demand {
|
||||
internal func assertNonZero(file: StaticString = #file,
|
||||
line: UInt = #line) {
|
||||
if self == .none {
|
||||
fatalError("API Violation: demand must not be zero", file: file, line: line)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
//
|
||||
// Locking.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 11.06.2019.
|
||||
//
|
||||
|
||||
#if canImport(Darwin)
|
||||
import Darwin
|
||||
#elseif canImport(Glibc)
|
||||
import Glibc
|
||||
#else
|
||||
#error("How to do locking on this platform?")
|
||||
#endif
|
||||
|
||||
@usableFromInline
|
||||
internal final class Lock {
|
||||
|
||||
@usableFromInline
|
||||
internal var _mutex = pthread_mutex_t()
|
||||
|
||||
@inlinable
|
||||
internal init(recursive: Bool) {
|
||||
var attrib = pthread_mutexattr_t()
|
||||
pthread_mutexattr_init(&attrib)
|
||||
if recursive {
|
||||
pthread_mutexattr_settype(&attrib, Int32(PTHREAD_MUTEX_RECURSIVE))
|
||||
}
|
||||
pthread_mutex_init(&_mutex, &attrib)
|
||||
}
|
||||
|
||||
@inlinable
|
||||
deinit {
|
||||
pthread_mutex_destroy(&_mutex)
|
||||
}
|
||||
|
||||
@inlinable
|
||||
internal func _lock() {
|
||||
pthread_mutex_lock(&_mutex)
|
||||
}
|
||||
|
||||
@inlinable
|
||||
internal func _unlock() {
|
||||
pthread_mutex_unlock(&_mutex)
|
||||
}
|
||||
|
||||
@inlinable
|
||||
internal func `do`<Result>(_ body: () throws -> Result) rethrows -> Result {
|
||||
_lock()
|
||||
defer { _unlock() }
|
||||
return try body()
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@
|
||||
/// specific values on-demand during tests.
|
||||
public final class PassthroughSubject<Output, Failure: Error>: Subject {
|
||||
|
||||
private let _lock = Lock(recursive: true)
|
||||
private let _lock = unfairRecursiveLock()
|
||||
|
||||
private var _completion: Subscribers.Completion<Failure>?
|
||||
|
||||
@@ -39,8 +39,8 @@ public final class PassthroughSubject<Output, Failure: Error>: Subject {
|
||||
}
|
||||
}
|
||||
|
||||
public func receive<SubscriberType: Subscriber>(subscriber: SubscriberType)
|
||||
where Output == SubscriberType.Input, Failure == SubscriberType.Failure
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Output == Downstream.Input, Failure == Downstream.Failure
|
||||
{
|
||||
_lock.do {
|
||||
if let completion = _completion {
|
||||
@@ -119,7 +119,7 @@ extension PassthroughSubject {
|
||||
fileprivate func request(_ demand: Subscribers.Demand) {
|
||||
precondition(demand > 0, "demand must not be zero")
|
||||
_parent?._lock.do {
|
||||
_demand = demand
|
||||
_demand += demand
|
||||
}
|
||||
_parent?._acknowledgeDownstreamDemand()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -38,9 +38,9 @@ public struct Deferred<DeferredPublisher: Publisher>: Publisher {
|
||||
/// - 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 Failure == SubscriberType.Failure,
|
||||
Output == SubscriberType.Input
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Failure == Downstream.Failure,
|
||||
Output == Downstream.Input
|
||||
{
|
||||
let deferredPublisher = createPublisher()
|
||||
deferredPublisher.subscribe(subscriber)
|
||||
|
||||
@@ -42,8 +42,8 @@ public struct Empty<Output, Failure: Error>: Publisher, Equatable {
|
||||
/// to the subscriber. If `false`, it never completes.
|
||||
public let completeImmediately: Bool
|
||||
|
||||
public func receive<SubscriberType: Subscriber>(subscriber: SubscriberType)
|
||||
where Output == SubscriberType.Input, Failure == SubscriberType.Failure
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Output == Downstream.Input, Failure == Downstream.Failure
|
||||
{
|
||||
subscriber.receive(subscription: Subscriptions.empty)
|
||||
if completeImmediately {
|
||||
|
||||
@@ -30,8 +30,8 @@ public struct Fail<Output, Failure: Error>: Publisher {
|
||||
/// The failure to send when terminating the publisher.
|
||||
public let error: Failure
|
||||
|
||||
public func receive<SubscriberType: Subscriber>(subscriber: SubscriberType)
|
||||
where Output == SubscriberType.Input, Failure == SubscriberType.Failure
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Output == Downstream.Input, Failure == Downstream.Failure
|
||||
{
|
||||
subscriber.receive(subscription: Subscriptions.empty)
|
||||
subscriber.receive(completion: .failure(error))
|
||||
|
||||
@@ -25,8 +25,8 @@ public struct Just<Output>: Publisher {
|
||||
self.output = output
|
||||
}
|
||||
|
||||
public func receive<SubscriberType: Subscriber>(subscriber: SubscriberType)
|
||||
where SubscriberType.Input == Output, SubscriberType.Failure == Never
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Input == Output, Downstream.Failure == Never
|
||||
{
|
||||
subscriber.receive(subscription: Inner(value: output, downstream: subscriber))
|
||||
}
|
||||
@@ -251,33 +251,43 @@ extension Just {
|
||||
}
|
||||
}
|
||||
|
||||
private final class Inner<SubscriberType: Subscriber>: Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable
|
||||
{
|
||||
private let _output: SubscriberType.Input
|
||||
private var _downstream: SubscriberType?
|
||||
extension Just {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Output
|
||||
{
|
||||
// NOTE: this class has been audited for thread safety.
|
||||
// Combine doesn't use any locking here.
|
||||
|
||||
init(value: SubscriberType.Input, downstream: SubscriberType) {
|
||||
_output = value
|
||||
_downstream = downstream
|
||||
}
|
||||
private var downstream: Downstream?
|
||||
private let value: Output
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
if let downstream = _downstream, demand > 0 {
|
||||
_ = downstream.receive(_output)
|
||||
downstream.receive(completion: .finished)
|
||||
_downstream = nil
|
||||
fileprivate init(value: Output, downstream: Downstream) {
|
||||
self.downstream = downstream
|
||||
self.value = value
|
||||
}
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
_downstream = nil
|
||||
}
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
demand.assertNonZero()
|
||||
guard let downstream = self.downstream else { return }
|
||||
self.downstream = nil
|
||||
_ = downstream.receive(value)
|
||||
downstream.receive(completion: .finished)
|
||||
}
|
||||
|
||||
var description: String { return "Just" }
|
||||
func cancel() {
|
||||
downstream = nil
|
||||
}
|
||||
|
||||
var customMirror: Mirror {
|
||||
return Mirror(self, unlabeledChildren: CollectionOfOne(_output))
|
||||
var description: String { return "Just" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
return Mirror(self, unlabeledChildren: CollectionOfOne(value))
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,8 +50,8 @@ extension Optional {
|
||||
self.output = output
|
||||
}
|
||||
|
||||
public func receive<SubscriberType: Subscriber>(subscriber: SubscriberType)
|
||||
where Output == SubscriberType.Input, Failure == SubscriberType.Failure
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Output == Downstream.Input, Failure == Downstream.Failure
|
||||
{
|
||||
if let output = output {
|
||||
subscriber.receive(subscription: Inner(value: output,
|
||||
@@ -74,34 +74,44 @@ extension Optional {
|
||||
#endif
|
||||
}
|
||||
|
||||
private final class Inner<SubscriberType: Subscriber>: Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable
|
||||
{
|
||||
private let _output: SubscriberType.Input
|
||||
private var _downstream: SubscriberType?
|
||||
extension Optional.OCombine {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Wrapped
|
||||
{
|
||||
// NOTE: this class has been audited for thread safety.
|
||||
// Combine doesn't use any locking here.
|
||||
|
||||
init(value: SubscriberType.Input, downstream: SubscriberType) {
|
||||
_output = value
|
||||
_downstream = downstream
|
||||
}
|
||||
private var downstream: Downstream?
|
||||
private let output: Wrapped
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
if let downstream = _downstream, demand > 0 {
|
||||
_ = downstream.receive(_output)
|
||||
downstream.receive(completion: .finished)
|
||||
_downstream = nil
|
||||
init(value: Wrapped, downstream: Downstream) {
|
||||
self.output = value
|
||||
self.downstream = downstream
|
||||
}
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
_downstream = nil
|
||||
}
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
demand.assertNonZero()
|
||||
guard let downstream = self.downstream else { return }
|
||||
self.downstream = nil
|
||||
_ = downstream.receive(output)
|
||||
downstream.receive(completion: .finished)
|
||||
}
|
||||
|
||||
var description: String { return "Optional" }
|
||||
func cancel() {
|
||||
downstream = nil
|
||||
}
|
||||
|
||||
var customMirror: Mirror {
|
||||
return Mirror(self, unlabeledChildren: CollectionOfOne(_output))
|
||||
var description: String { return "Optional" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
return Mirror(self, unlabeledChildren: CollectionOfOne(output))
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,7 +148,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,183 @@
|
||||
//
|
||||
// Publishers.Autoconnect.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 18/09/2019.
|
||||
//
|
||||
|
||||
extension ConnectablePublisher {
|
||||
|
||||
/// Automates the process of connecting or disconnecting from this connectable
|
||||
/// publisher.
|
||||
///
|
||||
/// Use `autoconnect()` to simplify working with `ConnectablePublisher` instances,
|
||||
/// such as those created with `makeConnectable()`.
|
||||
///
|
||||
/// let autoconnectedPublisher = somePublisher
|
||||
/// .makeConnectable()
|
||||
/// .autoconnect()
|
||||
/// .subscribe(someSubscriber)
|
||||
///
|
||||
/// - Returns: A publisher which automatically connects to its upstream connectable
|
||||
/// publisher.
|
||||
public func autoconnect() -> Publishers.Autoconnect<Self> {
|
||||
return .init(upstream: self)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that automatically connects and disconnects from this connectable
|
||||
/// publisher.
|
||||
public class Autoconnect<Upstream: ConnectablePublisher>: Publisher {
|
||||
|
||||
// NOTE: This class has been audited for thread safety
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
private enum State {
|
||||
case disconnected
|
||||
case connected(refcount: Int, connection: Cancellable)
|
||||
}
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public final let upstream: Upstream
|
||||
|
||||
private let lock = unfairLock()
|
||||
|
||||
private var state = State.disconnected
|
||||
|
||||
public init(upstream: Upstream) {
|
||||
self.upstream = upstream
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Input == Output, Downstream.Failure == Failure
|
||||
{
|
||||
let inner = Inner(parent: self, downstream: subscriber)
|
||||
lock.lock()
|
||||
switch state {
|
||||
case let .connected(refcount, connection):
|
||||
state = .connected(refcount: refcount + 1, connection: connection)
|
||||
lock.unlock()
|
||||
upstream.subscribe(inner)
|
||||
case .disconnected:
|
||||
lock.unlock()
|
||||
upstream.subscribe(inner)
|
||||
let connection = upstream.connect()
|
||||
lock.lock()
|
||||
state = .connected(refcount: 1, connection: connection)
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func willCancel() {
|
||||
lock.lock()
|
||||
switch state {
|
||||
case let .connected(refcount, connection):
|
||||
if refcount <= 1 {
|
||||
self.state = .disconnected
|
||||
lock.unlock()
|
||||
connection.cancel()
|
||||
} else {
|
||||
state = .connected(refcount: refcount - 1, connection: connection)
|
||||
lock.unlock()
|
||||
}
|
||||
case .disconnected:
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.Autoconnect {
|
||||
|
||||
private struct Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Output, Downstream.Failure == Failure
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
fileprivate let combineIdentifier = CombineIdentifier()
|
||||
|
||||
private let parent: Publishers.Autoconnect<Upstream>
|
||||
|
||||
private let downstream: Downstream
|
||||
|
||||
fileprivate init(parent: Publishers.Autoconnect<Upstream>,
|
||||
downstream: Downstream) {
|
||||
self.parent = parent
|
||||
self.downstream = downstream
|
||||
}
|
||||
|
||||
fileprivate func receive(subscription: Subscription) {
|
||||
let sideEffectSubscription = SideEffectSubscription(subscription,
|
||||
parent: parent)
|
||||
downstream.receive(subscription: sideEffectSubscription)
|
||||
}
|
||||
|
||||
fileprivate func receive(_ input: Upstream.Output) -> Subscribers.Demand {
|
||||
return downstream.receive(input)
|
||||
}
|
||||
|
||||
fileprivate func receive(completion: Subscribers.Completion<Failure>) {
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
|
||||
fileprivate var description: String { return "Autoconnect" }
|
||||
|
||||
fileprivate var customMirror: Mirror {
|
||||
let children: [Mirror.Child] = [
|
||||
("parent", parent),
|
||||
("downstream", downstream)
|
||||
]
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
fileprivate var playgroundDescription: Any { return description }
|
||||
}
|
||||
|
||||
private struct SideEffectSubscription
|
||||
: Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
{
|
||||
private let parent: Publishers.Autoconnect<Upstream>
|
||||
|
||||
private let upstreamSubscription: Subscription
|
||||
|
||||
fileprivate init(_ upstreamSubscription: Subscription,
|
||||
parent: Publishers.Autoconnect<Upstream>) {
|
||||
self.parent = parent
|
||||
self.upstreamSubscription = upstreamSubscription
|
||||
}
|
||||
|
||||
fileprivate func request(_ demand: Subscribers.Demand) {
|
||||
upstreamSubscription.request(demand)
|
||||
}
|
||||
|
||||
fileprivate func cancel() {
|
||||
parent.willCancel()
|
||||
upstreamSubscription.cancel()
|
||||
}
|
||||
|
||||
fileprivate var combineIdentifier: CombineIdentifier {
|
||||
return upstreamSubscription.combineIdentifier
|
||||
}
|
||||
|
||||
fileprivate var description: String {
|
||||
return String(describing: upstreamSubscription)
|
||||
}
|
||||
|
||||
var playgroundDescription: Any {
|
||||
return description
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,11 +33,11 @@ extension Publishers {
|
||||
/// - 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,
|
||||
SubscriberType.Input == Output
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Failure == Downstream.Failure,
|
||||
Downstream.Input == Output
|
||||
{
|
||||
let count = _Count<Upstream, SubscriberType>(downstream: subscriber)
|
||||
let count = _Count<Upstream, Downstream>(downstream: subscriber)
|
||||
upstream.subscribe(count)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,10 +35,10 @@ extension Publishers {
|
||||
/// - 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 Failure == SubscriberType.Failure, Output == SubscriberType.Input
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Failure == Downstream.Failure, Output == Downstream.Input
|
||||
{
|
||||
let decodeSubscriber = _Decode<Upstream, SubscriberType, Coder>(
|
||||
let decodeSubscriber = _Decode<Upstream, Downstream, Coder>(
|
||||
downstream: subscriber,
|
||||
decoder: _decoder
|
||||
)
|
||||
|
||||
@@ -26,8 +26,8 @@ extension Publishers {
|
||||
self.predicate = predicate
|
||||
}
|
||||
|
||||
public func receive<SubscriberType: Subscriber>(subscriber: SubscriberType)
|
||||
where Failure == SubscriberType.Failure, Output == SubscriberType.Input
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Failure == Downstream.Failure, Output == Downstream.Input
|
||||
{
|
||||
let inner = Inner(downstream: subscriber, predicate: catching(predicate))
|
||||
upstream.subscribe(inner)
|
||||
@@ -53,8 +53,8 @@ extension Publishers {
|
||||
self.predicate = predicate
|
||||
}
|
||||
|
||||
public func receive<SubscriberType: Subscriber>(subscriber: SubscriberType)
|
||||
where Output == SubscriberType.Input, SubscriberType.Failure == Error
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Output == Downstream.Input, Downstream.Failure == Error
|
||||
{
|
||||
let inner = Inner(downstream: subscriber, predicate: catching(predicate))
|
||||
upstream.subscribe(inner)
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,10 +37,10 @@ extension Publishers {
|
||||
/// - 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 Failure == SubscriberType.Failure, Output == SubscriberType.Input
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Failure == Downstream.Failure, Output == Downstream.Input
|
||||
{
|
||||
let encodeSubscriber = _Encode<Upstream, SubscriberType, Coder>(
|
||||
let encodeSubscriber = _Encode<Upstream, Downstream, Coder>(
|
||||
downstream: subscriber,
|
||||
encoder: encoder
|
||||
)
|
||||
|
||||
@@ -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<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Failure == Downstream.Failure,
|
||||
Upstream.Output == Downstream.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,295 @@
|
||||
//
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.First: Equatable where Upstream: Equatable {}
|
||||
|
||||
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,412 @@
|
||||
//
|
||||
// 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: Publisher>(
|
||||
maxPublishers: Subscribers.Demand = .unlimited,
|
||||
_ transform: @escaping (Output) -> Child
|
||||
) -> Publishers.FlatMap<Child, Self>
|
||||
where Result == Child.Output, Failure == Child.Failure {
|
||||
return Publishers.FlatMap(upstream: self,
|
||||
maxPublishers: maxPublishers,
|
||||
transform: transform)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
public struct FlatMap<Child: Publisher, Upstream: Publisher>: Publisher
|
||||
where 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>(subscriber: Downstream)
|
||||
where 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 = unfairLock()
|
||||
private let maxPublishers: Subscribers.Demand
|
||||
private let transform: (Input) -> 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, lockedDeactivateAndReturnChildrenToCancel())
|
||||
}
|
||||
|
||||
upstreamToCancel?.cancel()
|
||||
cancelChildren(childrenToCancel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Private implementation
|
||||
extension Publishers.FlatMap.Inner {
|
||||
|
||||
private func deactivate() {
|
||||
cancelChildren(lock.do(lockedDeactivateAndReturnChildrenToCancel))
|
||||
}
|
||||
|
||||
// Must be called with lock held.
|
||||
private func lockedDeactivateAndReturnChildrenToCancel() -> 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
|
||||
: .max(1)
|
||||
|
||||
if processTheQueue {
|
||||
processQueue()
|
||||
}
|
||||
|
||||
return demandResult
|
||||
}
|
||||
|
||||
private func demandForChild() -> Subscribers.Demand {
|
||||
return 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 {
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
fileprivate func cancel() {
|
||||
_upstreamSubscription?.cancel()
|
||||
_upstreamSubscription = nil
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.FlatMap.Inner.ChildSubscriber: Subscriber {
|
||||
|
||||
fileprivate 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 {}
|
||||
@@ -0,0 +1,42 @@
|
||||
//
|
||||
// Publishers.MakeConnectable.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 18/09/2019.
|
||||
//
|
||||
|
||||
extension Publisher where Failure == Never {
|
||||
|
||||
/// Creates a connectable wrapper around the publisher.
|
||||
///
|
||||
/// - Returns: A `ConnectablePublisher` wrapping this publisher.
|
||||
public func makeConnectable() -> Publishers.MakeConnectable<Self> {
|
||||
return .init(upstream: self)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
public struct MakeConnectable<Upstream: Publisher>: ConnectablePublisher {
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
private let inner: Multicast<Upstream, PassthroughSubject<Output, Failure>>
|
||||
|
||||
public init(upstream: Upstream) {
|
||||
inner = upstream.multicast(subject: .init())
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Failure == Failure, Downstream.Input == Output
|
||||
{
|
||||
inner.subscribe(subscriber)
|
||||
}
|
||||
|
||||
public func connect() -> Cancellable {
|
||||
return inner.connect()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -54,6 +54,12 @@ extension Publishers {
|
||||
self.upstream = upstream
|
||||
self.transform = transform
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Output == Downstream.Input, Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
upstream.subscribe(Inner(downstream: subscriber, map: transform))
|
||||
}
|
||||
}
|
||||
|
||||
/// A publisher that transforms all elements from the upstream publisher
|
||||
@@ -78,12 +84,6 @@ extension Publishers {
|
||||
}
|
||||
|
||||
extension Publishers.Map {
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Output == Downstream.Input, Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
let inner = Inner(downstream: subscriber, transform: catching(transform))
|
||||
upstream.subscribe(inner)
|
||||
}
|
||||
|
||||
public func map<Result>(
|
||||
_ transform: @escaping (Output) -> Result
|
||||
@@ -103,8 +103,7 @@ extension Publishers.TryMap {
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Output == Downstream.Input, Downstream.Failure == Error
|
||||
{
|
||||
let inner = Inner(downstream: subscriber, transform: catching(transform))
|
||||
upstream.subscribe(inner)
|
||||
upstream.subscribe(Inner(downstream: subscriber, map: transform))
|
||||
}
|
||||
|
||||
public func map<Result>(
|
||||
@@ -120,89 +119,154 @@ extension Publishers.TryMap {
|
||||
}
|
||||
}
|
||||
|
||||
private class _Map<Upstream: Publisher, Downstream: Subscriber>
|
||||
: OperatorSubscription<Downstream>
|
||||
{
|
||||
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(_ input: Input) -> Subscribers.Demand {
|
||||
switch _transform?(input) {
|
||||
case .success(let output)?:
|
||||
return downstream.receive(output)
|
||||
case .failure(let error)?:
|
||||
downstream.receive(completion: .failure(error))
|
||||
_transform = nil
|
||||
return .none
|
||||
case nil:
|
||||
return .none
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.Map {
|
||||
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: _Map<Upstream, Downstream>,
|
||||
private struct Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
CustomStringConvertible,
|
||||
Subscriber
|
||||
where Downstream.Failure == Upstream.Failure
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Output, Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
private let downstream: Downstream
|
||||
|
||||
private let map: (Input) -> Output
|
||||
|
||||
let combineIdentifier = CombineIdentifier()
|
||||
|
||||
fileprivate init(downstream: Downstream, map: @escaping (Input) -> Output) {
|
||||
self.downstream = downstream
|
||||
self.map = map
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
downstream.receive(subscription: subscription)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
return downstream.receive(map(input))
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
|
||||
var description: String { return "Map" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
return Mirror(self, children: EmptyCollection())
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.TryMap {
|
||||
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: _Map<Upstream, Downstream>,
|
||||
: Subscriber,
|
||||
Subscription,
|
||||
CustomStringConvertible,
|
||||
Subscriber
|
||||
where Downstream.Failure == Error
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Output, Downstream.Failure == Error
|
||||
{
|
||||
// NOTE: This class has been audited for thread-safety
|
||||
|
||||
typealias Input = Upstream.Output
|
||||
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
private let downstream: Downstream
|
||||
|
||||
private let map: (Input) throws -> Output
|
||||
|
||||
private var status = SubscriptionStatus.awaitingSubscription
|
||||
|
||||
private let lock = unfairLock()
|
||||
|
||||
let combineIdentifier = CombineIdentifier()
|
||||
|
||||
fileprivate init(downstream: Downstream,
|
||||
map: @escaping (Input) throws -> Output) {
|
||||
self.downstream = downstream
|
||||
self.map = map
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
upstreamSubscription = subscription
|
||||
lock.lock()
|
||||
guard case .awaitingSubscription = status else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
status = .subscribed(subscription)
|
||||
lock.unlock()
|
||||
downstream.receive(subscription: self)
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
if !isCompleted {
|
||||
_transform = nil
|
||||
downstream.receive(completion: completion.eraseError())
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
do {
|
||||
return try downstream.receive(map(input))
|
||||
} catch {
|
||||
lock.lock()
|
||||
let subscription: Subscription?
|
||||
switch status {
|
||||
case let .subscribed(upstreamSubscription):
|
||||
subscription = upstreamSubscription
|
||||
case .awaitingSubscription, .terminal:
|
||||
subscription = nil
|
||||
}
|
||||
status = .terminal
|
||||
lock.unlock()
|
||||
subscription?.cancel()
|
||||
downstream.receive(completion: .failure(error))
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
upstreamSubscription?.request(demand)
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
lock.lock()
|
||||
guard case .subscribed = status else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
status = .terminal
|
||||
lock.unlock()
|
||||
downstream.receive(completion: completion.eraseError())
|
||||
}
|
||||
|
||||
override func cancel() {
|
||||
_transform = nil
|
||||
super.cancel()
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
lock.lock()
|
||||
guard case let .subscribed(subscription) = status else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
lock.unlock()
|
||||
subscription.request(demand)
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
guard case let .subscribed(subscription) = status else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
status = .terminal
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
}
|
||||
|
||||
var description: String { return "TryMap" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
return Mirror(self, children: EmptyCollection())
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,15 +32,11 @@ extension Publishers {
|
||||
/// - 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 Failure == SubscriberType.Failure,
|
||||
Upstream.Output == SubscriberType.Input
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Failure == Downstream.Failure,
|
||||
Upstream.Output == Downstream.Input
|
||||
{
|
||||
let mapErrorSubscriber = _MapError<Upstream, SubscriberType>(
|
||||
downstream: subscriber,
|
||||
transform: transform
|
||||
)
|
||||
upstream.subscribe(mapErrorSubscriber)
|
||||
upstream.subscribe(Inner(downstream: subscriber, map: transform))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,41 +60,50 @@ extension Publisher {
|
||||
}
|
||||
}
|
||||
|
||||
private final class _MapError<Upstream: Publisher, Downstream: Subscriber>
|
||||
: OperatorSubscription<Downstream>,
|
||||
Subscriber,
|
||||
CustomStringConvertible
|
||||
where Upstream.Output == Downstream.Input
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
typealias Failure = Upstream.Failure
|
||||
typealias Output = Downstream.Input
|
||||
extension Publishers.MapError {
|
||||
|
||||
private let _transform: (Upstream.Failure) -> Downstream.Failure
|
||||
private struct Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Upstream.Output == Downstream.Input
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
var description: String { return "MapError" }
|
||||
private let downstream: Downstream
|
||||
private let map: (Upstream.Failure) -> Downstream.Failure
|
||||
|
||||
init(downstream: Downstream,
|
||||
transform: @escaping (Upstream.Failure) -> Downstream.Failure) {
|
||||
self._transform = transform
|
||||
super.init(downstream: downstream)
|
||||
}
|
||||
let combineIdentifier = CombineIdentifier()
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
upstreamSubscription = subscription
|
||||
downstream.receive(subscription: subscription)
|
||||
}
|
||||
var description: String { return "MapError" }
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
return downstream.receive(input)
|
||||
}
|
||||
var customMirror: Mirror { return Mirror(self, children: EmptyCollection()) }
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
switch completion {
|
||||
case .finished:
|
||||
downstream.receive(completion: .finished)
|
||||
case .failure(let error):
|
||||
downstream.receive(completion: .failure(_transform(error)))
|
||||
var playgroundDescription: Any { return description }
|
||||
|
||||
init(downstream: Downstream,
|
||||
map: @escaping (Upstream.Failure) -> Downstream.Failure) {
|
||||
self.downstream = downstream
|
||||
self.map = map
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
downstream.receive(subscription: subscription)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
return downstream.receive(input)
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
switch completion {
|
||||
case .finished:
|
||||
downstream.receive(completion: .finished)
|
||||
case .failure(let error):
|
||||
downstream.receive(completion: .failure(map(error)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,9 +28,10 @@ extension Publishers {
|
||||
|
||||
public final class Multicast<Upstream: Publisher, SubjectType: Subject>
|
||||
: ConnectablePublisher
|
||||
where Upstream.Failure == SubjectType.Failure,
|
||||
Upstream.Output == SubjectType.Output
|
||||
where Upstream.Failure == SubjectType.Failure,
|
||||
Upstream.Output == SubjectType.Output
|
||||
{
|
||||
// NOTE: This class has been audited for thread safety
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
@@ -40,7 +41,22 @@ extension Publishers {
|
||||
|
||||
public let createSubject: () -> SubjectType
|
||||
|
||||
private lazy var _subject: SubjectType = self.createSubject()
|
||||
private let lock = unfairLock()
|
||||
|
||||
private var subject: SubjectType?
|
||||
|
||||
private var lazySubject: SubjectType {
|
||||
lock.lock()
|
||||
if let subject = subject {
|
||||
lock.unlock()
|
||||
return subject
|
||||
}
|
||||
|
||||
let subject = createSubject()
|
||||
self.subject = subject
|
||||
lock.unlock()
|
||||
return subject
|
||||
}
|
||||
|
||||
public init(upstream: Upstream, createSubject: @escaping () -> SubjectType) {
|
||||
self.upstream = upstream
|
||||
@@ -51,18 +67,11 @@ extension Publishers {
|
||||
where SubjectType.Failure == Downstream.Failure,
|
||||
SubjectType.Output == Downstream.Input
|
||||
{
|
||||
_subject.subscribe(Inner(downstream: subscriber))
|
||||
lazySubject.subscribe(Inner(parent: self, downstream: subscriber))
|
||||
}
|
||||
|
||||
public func connect() -> Cancellable {
|
||||
|
||||
let subscriber = SubjectSubscriber(_subject)
|
||||
|
||||
upstream.subscribe(subscriber)
|
||||
|
||||
return AnyCancellable {
|
||||
subscriber.downstreamSubject = nil
|
||||
}
|
||||
return upstream.subscribe(lazySubject)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,32 +79,101 @@ extension Publishers {
|
||||
extension Publishers.Multicast {
|
||||
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: OperatorSubscription<Downstream>,
|
||||
Subscriber,
|
||||
: Subscriber,
|
||||
Subscription,
|
||||
CustomStringConvertible,
|
||||
Subscription
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Upstream.Output == Downstream.Input, Upstream.Failure == Downstream.Failure
|
||||
{
|
||||
// NOTE: This class has been audited for thread safety
|
||||
|
||||
typealias Input = Upstream.Output
|
||||
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
var description: String { return "Multicast" }
|
||||
private enum State {
|
||||
case ready(upstream: Upstream, downstream: Downstream)
|
||||
case subscribed(upstream: Upstream,
|
||||
downstream: Downstream,
|
||||
subjectSubscription: Subscription)
|
||||
case terminal
|
||||
}
|
||||
|
||||
private let lock = unfairLock()
|
||||
|
||||
private var state: State
|
||||
|
||||
fileprivate init(parent: Publishers.Multicast<Upstream, SubjectType>,
|
||||
downstream: Downstream) {
|
||||
state = .ready(upstream: parent.upstream, downstream: downstream)
|
||||
}
|
||||
|
||||
fileprivate var description: String { return "Multicast" }
|
||||
|
||||
fileprivate var customMirror: Mirror {
|
||||
return Mirror(self, children: EmptyCollection())
|
||||
}
|
||||
|
||||
fileprivate var playgroundDescription: Any { return description }
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
upstreamSubscription = subscription
|
||||
lock.lock()
|
||||
guard case let .ready(upstream, downstream) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
state = .subscribed(upstream: upstream,
|
||||
downstream: downstream,
|
||||
subjectSubscription: subscription)
|
||||
lock.unlock()
|
||||
downstream.receive(subscription: self)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
return downstream.receive(input)
|
||||
lock.lock()
|
||||
guard case let .subscribed(_, downstream, subjectSubscription) = state else {
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
lock.unlock()
|
||||
let newDemand = downstream.receive(input)
|
||||
if newDemand > 0 {
|
||||
subjectSubscription.request(newDemand)
|
||||
}
|
||||
return .none
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
lock.lock()
|
||||
guard case let .subscribed(_, downstream, _) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
state = .terminal
|
||||
lock.unlock()
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
upstreamSubscription?.request(demand)
|
||||
lock.lock()
|
||||
guard case let .subscribed(_, _, subjectSubscription) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
lock.unlock()
|
||||
subjectSubscription.request(demand)
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
guard case let .subscribed(_, _, subjectSubscription) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
state = .terminal
|
||||
lock.unlock()
|
||||
subjectSubscription.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,8 +43,8 @@ extension Publishers {
|
||||
self.stream = stream
|
||||
}
|
||||
|
||||
public func receive<SubscriberType: Subscriber>(subscriber: SubscriberType)
|
||||
where Failure == SubscriberType.Failure, Output == SubscriberType.Input
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Failure == Downstream.Failure, Output == Downstream.Input
|
||||
{
|
||||
let inner = Inner(downstream: subscriber, prefix: prefix, stream: stream)
|
||||
upstream.subscribe(inner)
|
||||
@@ -65,99 +65,93 @@ extension Publisher {
|
||||
}
|
||||
}
|
||||
|
||||
private final class Inner<Downstream: Subscriber>: Subscriber,
|
||||
Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable
|
||||
{
|
||||
typealias Input = Downstream.Input
|
||||
typealias Failure = Downstream.Failure
|
||||
extension Publishers.Print {
|
||||
private final class Inner<Downstream: Subscriber>: Subscriber,
|
||||
Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable
|
||||
{
|
||||
typealias Input = Downstream.Input
|
||||
typealias Failure = Downstream.Failure
|
||||
|
||||
private var _downstream: Downstream
|
||||
private let _prefix: String
|
||||
private var _stream: TextOutputStream
|
||||
private var _upstreamSubscription: Subscription?
|
||||
private let _printerLock = Lock(recursive: false)
|
||||
/// A concrete type wrapper around an abstract stream.
|
||||
private struct PrintTarget: TextOutputStream {
|
||||
|
||||
init(downstream: Downstream, prefix: String, stream: TextOutputStream?) {
|
||||
_downstream = downstream
|
||||
_prefix = prefix
|
||||
_stream = stream ?? StdoutStream()
|
||||
}
|
||||
var stream: TextOutputStream
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
_log("receive subscription", value: subscription)
|
||||
_upstreamSubscription = subscription
|
||||
_downstream.receive(subscription: self)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
_log("receive value", value: input)
|
||||
let demand = _downstream.receive(input)
|
||||
_logDemand(demand, synchronous: true)
|
||||
return demand
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
switch completion {
|
||||
case .finished:
|
||||
_log("receive finished")
|
||||
case .failure(let error):
|
||||
_log("receive error", value: error)
|
||||
mutating func write(_ string: String) {
|
||||
stream.write(string)
|
||||
}
|
||||
}
|
||||
_downstream.receive(completion: completion)
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
_logDemand(demand, synchronous: false)
|
||||
_upstreamSubscription?.request(demand)
|
||||
}
|
||||
private var downstream: Downstream
|
||||
private let prefix: String
|
||||
private var stream: PrintTarget?
|
||||
private var subscription: Subscription?
|
||||
private let lock = unfairLock()
|
||||
|
||||
func cancel() {
|
||||
_log("receive cancel")
|
||||
_upstreamSubscription?.cancel()
|
||||
_upstreamSubscription = nil
|
||||
}
|
||||
|
||||
var description: String { return "Print" }
|
||||
|
||||
var customMirror: Mirror { return Mirror(self, children: EmptyCollection()) }
|
||||
|
||||
private func _log(_ description: String,
|
||||
value: Any? = nil,
|
||||
additionalInfo: String = "") {
|
||||
_printerLock.do {
|
||||
if !_prefix.isEmpty {
|
||||
_stream.write(_prefix)
|
||||
_stream.write(": ")
|
||||
}
|
||||
_stream.write(description)
|
||||
if let value = value {
|
||||
_stream.write(": (")
|
||||
_stream.write(String(describing: value))
|
||||
_stream.write(")")
|
||||
}
|
||||
if !additionalInfo.isEmpty {
|
||||
_stream.write(" (")
|
||||
_stream.write(additionalInfo)
|
||||
_stream.write(")")
|
||||
}
|
||||
_stream.write("\n")
|
||||
init(downstream: Downstream, prefix: String, stream: TextOutputStream?) {
|
||||
self.downstream = downstream
|
||||
self.prefix = prefix.isEmpty ? "" : "\(prefix): "
|
||||
self.stream = stream.map(PrintTarget.init)
|
||||
}
|
||||
}
|
||||
|
||||
private func _logDemand(_ demand: Subscribers.Demand, synchronous: Bool) {
|
||||
let synchronouslyStr = synchronous ? "synchronous" : ""
|
||||
if let max = demand.max {
|
||||
_log("request max", value: max, additionalInfo: synchronouslyStr)
|
||||
} else {
|
||||
_log("request unlimited", additionalInfo: synchronouslyStr)
|
||||
func receive(subscription: Subscription) {
|
||||
log("\(prefix)receive subscription: (\(subscription))")
|
||||
lock.do {
|
||||
self.subscription = subscription
|
||||
}
|
||||
downstream.receive(subscription: self)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
log("\(prefix)receive value: (\(input))")
|
||||
let demand = downstream.receive(input)
|
||||
|
||||
if let max = demand.max {
|
||||
log("\(prefix)request max: (\(max)) (synchronous)")
|
||||
} else {
|
||||
log("\(prefix)request unlimited (synchronous)")
|
||||
}
|
||||
|
||||
return demand
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
switch completion {
|
||||
case .finished:
|
||||
log("\(prefix)receive finished")
|
||||
case .failure(let error):
|
||||
log("\(prefix)receive error: (\(error))")
|
||||
}
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
if let max = demand.max {
|
||||
log("\(prefix)request max: (\(max))")
|
||||
} else {
|
||||
log("\(prefix)request unlimited")
|
||||
}
|
||||
subscription?.request(demand)
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
log("\(prefix)receive cancel")
|
||||
subscription?.cancel()
|
||||
subscription = nil
|
||||
}
|
||||
|
||||
var description: String { return "Print" }
|
||||
|
||||
var customMirror: Mirror { return Mirror(self, children: EmptyCollection()) }
|
||||
|
||||
private func log(_ text: String) {
|
||||
if var stream = stream {
|
||||
Swift.print(text, to: &stream)
|
||||
} else {
|
||||
Swift.print("", text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct StdoutStream: TextOutputStream {
|
||||
mutating func write(_ string: String) {
|
||||
print(string, terminator: "")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
//
|
||||
// Publishers.ReplaceError.swift
|
||||
// OpenCombine
|
||||
//
|
||||
// Created by Bogdan Vlad on 8/29/19.
|
||||
//
|
||||
|
||||
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 .init(upstream: self, output: output)
|
||||
}
|
||||
}
|
||||
|
||||
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<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Output == Downstream.Input, Downstream.Failure == Failure
|
||||
{
|
||||
upstream.subscribe(Inner(downstream: subscriber, output: output))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.ReplaceError: Equatable
|
||||
where Upstream: Equatable, Upstream.Output: Equatable
|
||||
{}
|
||||
|
||||
extension Publishers.ReplaceError {
|
||||
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Upstream.Output == Downstream.Input
|
||||
{
|
||||
// NOTE: this class has been audited for thread safety.
|
||||
|
||||
typealias Input = Upstream.Output
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
private let output: Upstream.Output
|
||||
private let downstream: Downstream
|
||||
private var status = SubscriptionStatus.awaitingSubscription
|
||||
private var terminated = false
|
||||
private var pendingDemand = Subscribers.Demand.none
|
||||
private var lock = unfairLock()
|
||||
|
||||
fileprivate init(downstream: Downstream, output: Upstream.Output) {
|
||||
self.downstream = downstream
|
||||
self.output = output
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard case .awaitingSubscription = status else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
status = .subscribed(subscription)
|
||||
lock.unlock()
|
||||
downstream.receive(subscription: self)
|
||||
}
|
||||
|
||||
func receive(_ input: Upstream.Output) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
guard case .subscribed = status else {
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
pendingDemand -= 1
|
||||
lock.unlock()
|
||||
let demand = downstream.receive(input)
|
||||
guard demand > 0 else {
|
||||
return .none
|
||||
}
|
||||
lock.lock()
|
||||
pendingDemand += demand
|
||||
lock.unlock()
|
||||
return demand
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Upstream.Failure>) {
|
||||
switch completion {
|
||||
case .finished:
|
||||
downstream.receive(completion: .finished)
|
||||
case .failure:
|
||||
lock.lock()
|
||||
// If there was no demand from downstream,
|
||||
// ReplaceError does not forward the value that
|
||||
// replaces the error until it is requested.
|
||||
guard pendingDemand > 0 else {
|
||||
terminated = true
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
lock.unlock()
|
||||
_ = downstream.receive(output)
|
||||
downstream.receive(completion: .finished)
|
||||
}
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
demand.assertNonZero()
|
||||
lock.lock()
|
||||
if terminated {
|
||||
status = .terminal
|
||||
lock.unlock()
|
||||
_ = downstream.receive(output)
|
||||
downstream.receive(completion: .finished)
|
||||
return
|
||||
}
|
||||
pendingDemand += demand
|
||||
guard case let .subscribed(subscription) = status else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
lock.unlock()
|
||||
subscription.request(demand)
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
guard case let .subscribed(subscription) = status else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
status = .terminal
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
}
|
||||
|
||||
var description: String { return "ReplaceError" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
return Mirror(self, children: EmptyCollection())
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
@@ -25,11 +25,13 @@ extension Publishers {
|
||||
self.sequence = sequence
|
||||
}
|
||||
|
||||
public func receive<SubscriberType: Subscriber>(subscriber: SubscriberType)
|
||||
where Failure == SubscriberType.Failure,
|
||||
Elements.Element == SubscriberType.Input
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Failure == Downstream.Failure,
|
||||
Elements.Element == Downstream.Input
|
||||
{
|
||||
if let inner = Inner(downstream: subscriber, sequence: sequence) {
|
||||
var iterator = sequence.makeIterator()
|
||||
if iterator.next() != nil {
|
||||
let inner = Inner(downstream: subscriber, sequence: sequence)
|
||||
subscriber.receive(subscription: inner)
|
||||
} else {
|
||||
subscriber.receive(subscription: Subscriptions.empty)
|
||||
@@ -44,66 +46,89 @@ extension Publishers.Sequence {
|
||||
private final class Inner<Downstream: Subscriber, Elements: Sequence, Failure>
|
||||
: Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Elements.Element,
|
||||
Downstream.Failure == Failure
|
||||
{
|
||||
// NOTE: This class has been audited for thread-safety
|
||||
|
||||
typealias Iterator = Elements.Iterator
|
||||
typealias Element = Elements.Element
|
||||
|
||||
private var _downstream: Downstream?
|
||||
private var _sequence: Elements?
|
||||
private var _iterator: Iterator?
|
||||
private var _nextValue: Element?
|
||||
private var sequence: Elements?
|
||||
private var downstream: Downstream?
|
||||
private var iterator: Iterator
|
||||
private var next: Element?
|
||||
private var pendingDemand = Subscribers.Demand.none
|
||||
private var recursion = false
|
||||
private var lock = unfairLock()
|
||||
|
||||
init?(downstream: Downstream, sequence: Elements) {
|
||||
|
||||
// Early exit if the sequence is empty
|
||||
var iterator = sequence.makeIterator()
|
||||
guard iterator.next() != nil else { return nil }
|
||||
|
||||
_downstream = downstream
|
||||
_sequence = sequence
|
||||
_iterator = sequence.makeIterator()
|
||||
_nextValue = iterator.next()
|
||||
fileprivate init(downstream: Downstream, sequence: Elements) {
|
||||
self.sequence = sequence
|
||||
self.downstream = downstream
|
||||
self.iterator = sequence.makeIterator()
|
||||
next = iterator.next()
|
||||
}
|
||||
|
||||
var description: String {
|
||||
return _sequence.map(String.init(describing:)) ?? "Sequence"
|
||||
return sequence.map(String.init(describing:)) ?? "Sequence"
|
||||
}
|
||||
|
||||
var customMirror: Mirror {
|
||||
let children: CollectionOfOne<(label: String?, value: Any)> =
|
||||
CollectionOfOne(("sequence", _sequence ?? [Element]()))
|
||||
let children =
|
||||
CollectionOfOne<Mirror.Child>(("sequence", sequence ?? [Element]()))
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
lock.lock()
|
||||
guard downstream != nil else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
pendingDemand += demand
|
||||
if recursion {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
|
||||
guard let downstream = _downstream else { return }
|
||||
while let downstream = self.downstream, pendingDemand > 0 {
|
||||
if let current = self.next {
|
||||
pendingDemand -= 1
|
||||
|
||||
var demand = demand
|
||||
|
||||
while demand > 0 {
|
||||
if let nextValue = _nextValue {
|
||||
demand += downstream.receive(nextValue)
|
||||
demand -= 1
|
||||
// Combine calls next() while the lock is held.
|
||||
// It is possible to engineer a custom Sequence that would cause
|
||||
// a dedlock here, but it would be something insane.
|
||||
let next = iterator.next()
|
||||
recursion = true
|
||||
lock.unlock()
|
||||
let additionalDemand = downstream.receive(current)
|
||||
lock.lock()
|
||||
recursion = false
|
||||
pendingDemand += additionalDemand
|
||||
self.next = next
|
||||
}
|
||||
|
||||
_nextValue = _iterator?.next()
|
||||
|
||||
if _nextValue == nil {
|
||||
_downstream?.receive(completion: .finished)
|
||||
cancel()
|
||||
break
|
||||
if next == nil {
|
||||
self.downstream = nil
|
||||
self.sequence = nil
|
||||
lock.unlock()
|
||||
downstream.receive(completion: .finished)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
lock.unlock()
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
_downstream = nil
|
||||
_iterator = nil
|
||||
_sequence = nil
|
||||
lock.lock()
|
||||
downstream = nil
|
||||
sequence = nil
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,8 +31,7 @@ extension Publishers {
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Failure == Failure, Downstream.Input == Output
|
||||
{
|
||||
let inner = Inner(downstream: subscriber)
|
||||
upstream.subscribe(inner)
|
||||
upstream.subscribe(Inner(downstream: subscriber))
|
||||
}
|
||||
|
||||
public func setFailureType<NewFailure: Error>(
|
||||
@@ -63,12 +62,20 @@ extension Publisher where Failure == Never {
|
||||
}
|
||||
|
||||
extension Publishers.SetFailureType {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: OperatorSubscription<Downstream>,
|
||||
Subscriber,
|
||||
CustomStringConvertible
|
||||
where Upstream.Output == Downstream.Input
|
||||
private struct Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Upstream.Output == Downstream.Input, Failure == Downstream.Failure
|
||||
{
|
||||
private let downstream: Downstream
|
||||
let combineIdentifier = CombineIdentifier()
|
||||
|
||||
fileprivate init(downstream: Downstream) {
|
||||
self.downstream = downstream
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
downstream.receive(subscription: subscription)
|
||||
}
|
||||
@@ -82,5 +89,11 @@ extension Publishers.SetFailureType {
|
||||
}
|
||||
|
||||
var description: String { return "SetFailureType" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
return Mirror(self, children: EmptyCollection())
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
//
|
||||
// Publishers.Share
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 18/09/2019.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Returns a publisher as a class instance.
|
||||
///
|
||||
/// The downstream subscriber receieves elements and completion states unchanged from
|
||||
/// the upstream publisher. Use this operator when you want to use
|
||||
/// reference semantics, such as storing a publisher instance in a property.
|
||||
///
|
||||
/// - Returns: A class instance that republishes its upstream publisher.
|
||||
public func share() -> Publishers.Share<Self> {
|
||||
return .init(upstream: self)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher implemented as a class, which otherwise behaves like its upstream
|
||||
/// publisher.
|
||||
public final class Share<Upstream: Publisher>: Publisher, Equatable {
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
private typealias MulticastSubject = PassthroughSubject<Output, Failure>
|
||||
|
||||
private let inner: Autoconnect<Multicast<Upstream, MulticastSubject>>
|
||||
|
||||
public let upstream: Upstream
|
||||
|
||||
public init(upstream: Upstream) {
|
||||
self.inner = upstream.multicast(subject: .init()).autoconnect()
|
||||
self.upstream = upstream
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Input == Output, Downstream.Failure == Failure
|
||||
{
|
||||
inner.subscribe(subscriber)
|
||||
}
|
||||
|
||||
public static func == (lhs: Share, rhs: Share) -> Bool {
|
||||
return lhs === rhs
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,8 +77,8 @@ extension Result {
|
||||
self.init(.failure(failure))
|
||||
}
|
||||
|
||||
public func receive<SubscriberType: Subscriber>(subscriber: SubscriberType)
|
||||
where SubscriberType.Input == Success, SubscriberType.Failure == Failure
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Input == Success, Downstream.Failure == Failure
|
||||
{
|
||||
switch result {
|
||||
case .success(let value):
|
||||
@@ -115,34 +115,44 @@ extension Result {
|
||||
#endif
|
||||
}
|
||||
|
||||
private final class Inner<SubscriberType: Subscriber>: Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable
|
||||
{
|
||||
private let _output: SubscriberType.Input
|
||||
private var _downstream: SubscriberType?
|
||||
extension Result.OCombine {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Success, Downstream.Failure == Failure
|
||||
{
|
||||
// NOTE: this class has been audited for thread safety.
|
||||
// Combine doesn't use any locking here.
|
||||
|
||||
init(value: SubscriberType.Input, downstream: SubscriberType) {
|
||||
_output = value
|
||||
_downstream = downstream
|
||||
}
|
||||
private var downstream: Downstream?
|
||||
private let output: Success
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
if let downstream = _downstream, demand > 0 {
|
||||
_ = downstream.receive(_output)
|
||||
downstream.receive(completion: .finished)
|
||||
_downstream = nil
|
||||
init(value: Success, downstream: Downstream) {
|
||||
self.output = value
|
||||
self.downstream = downstream
|
||||
}
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
_downstream = nil
|
||||
}
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
demand.assertNonZero()
|
||||
guard let downstream = self.downstream else { return }
|
||||
self.downstream = nil
|
||||
_ = downstream.receive(output)
|
||||
downstream.receive(completion: .finished)
|
||||
}
|
||||
|
||||
var description: String { return "Once" }
|
||||
func cancel() {
|
||||
downstream = nil
|
||||
}
|
||||
|
||||
var customMirror: Mirror {
|
||||
return Mirror(self, unlabeledChildren: CollectionOfOne(_output))
|
||||
var description: String { return "Once" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
return Mirror(self, unlabeledChildren: CollectionOfOne(output))
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) } }
|
||||
}
|
||||
|
||||
@@ -13,21 +13,24 @@ extension Subscribers {
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
{
|
||||
// NOTE: this class has been audited for thread safety.
|
||||
// Combine doesn't use any locking here.
|
||||
|
||||
public typealias Failure = Never
|
||||
|
||||
public private(set) var object: Root?
|
||||
|
||||
public let keyPath: ReferenceWritableKeyPath<Root, Input>
|
||||
|
||||
private var _upstreamSubscription: Subscription?
|
||||
private var status = SubscriptionStatus.awaitingSubscription
|
||||
|
||||
public var description: String { return "Assign \(Root.self)." }
|
||||
|
||||
public var customMirror: Mirror {
|
||||
let children: [(label: String?, value: Any)] = [
|
||||
(label: "object", value: object as Any),
|
||||
(label: "keyPath", value: keyPath),
|
||||
(label: "upstreamSubscription", value: _upstreamSubscription as Any)
|
||||
let children: [Mirror.Child] = [
|
||||
("object", object as Any),
|
||||
("keyPath", keyPath),
|
||||
("status", status as Any)
|
||||
]
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
@@ -40,17 +43,21 @@ extension Subscribers {
|
||||
}
|
||||
|
||||
public func receive(subscription: Subscription) {
|
||||
if _upstreamSubscription == nil {
|
||||
_upstreamSubscription = subscription
|
||||
subscription.request(.unlimited)
|
||||
} else {
|
||||
switch status {
|
||||
case .subscribed, .terminal:
|
||||
subscription.cancel()
|
||||
case .awaitingSubscription:
|
||||
status = .subscribed(subscription)
|
||||
subscription.request(.unlimited)
|
||||
}
|
||||
}
|
||||
|
||||
public func receive(_ value: Input) -> Subscribers.Demand {
|
||||
if _upstreamSubscription != nil {
|
||||
switch status {
|
||||
case .subscribed:
|
||||
object?[keyPath: keyPath] = value
|
||||
case .awaitingSubscription, .terminal:
|
||||
break
|
||||
}
|
||||
return .none
|
||||
}
|
||||
@@ -60,8 +67,11 @@ extension Subscribers {
|
||||
}
|
||||
|
||||
public func cancel() {
|
||||
_upstreamSubscription?.cancel()
|
||||
_upstreamSubscription = nil
|
||||
guard case let .subscribed(subscription) = status else {
|
||||
return
|
||||
}
|
||||
subscription.cancel()
|
||||
status = .terminal
|
||||
object = nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,13 +15,16 @@ extension Subscribers {
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
{
|
||||
// NOTE: this class has been audited for thread safety.
|
||||
// Combine doesn't use any locking here.
|
||||
|
||||
/// The closure to execute on receipt of a value.
|
||||
public let receiveValue: (Input) -> Void
|
||||
|
||||
/// The closure to execute on completion.
|
||||
public let receiveCompletion: (Subscribers.Completion<Failure>) -> Void
|
||||
|
||||
private var _upstreamSubscription: Subscription?
|
||||
private var status = SubscriptionStatus.awaitingSubscription
|
||||
|
||||
public var description: String { return "Sink" }
|
||||
|
||||
@@ -45,11 +48,12 @@ extension Subscribers {
|
||||
}
|
||||
|
||||
public func receive(subscription: Subscription) {
|
||||
if _upstreamSubscription == nil {
|
||||
_upstreamSubscription = subscription
|
||||
subscription.request(.unlimited)
|
||||
} else {
|
||||
switch status {
|
||||
case .subscribed, .terminal:
|
||||
subscription.cancel()
|
||||
case .awaitingSubscription:
|
||||
status = .subscribed(subscription)
|
||||
subscription.request(.unlimited)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,11 +64,15 @@ extension Subscribers {
|
||||
|
||||
public func receive(completion: Subscribers.Completion<Failure>) {
|
||||
receiveCompletion(completion)
|
||||
status = .terminal
|
||||
}
|
||||
|
||||
public func cancel() {
|
||||
_upstreamSubscription?.cancel()
|
||||
_upstreamSubscription = nil
|
||||
guard case let .subscribed(subscription) = status else {
|
||||
return
|
||||
}
|
||||
subscription.cancel()
|
||||
status = .terminal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,345 @@
|
||||
//
|
||||
// DispatchQueue.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 21.08.2019.
|
||||
//
|
||||
|
||||
import Dispatch
|
||||
import OpenCombine
|
||||
|
||||
extension DispatchQueue {
|
||||
|
||||
/// A namespace for disambiguation when both OpenCombine and Combine are imported.
|
||||
///
|
||||
/// Combine extends `DispatchQueue` with new methods and nested types.
|
||||
/// If you import both OpenCombine and Combine (either explicitly or implicitly,
|
||||
/// e. g. when importing Foundation), you will not be able
|
||||
/// to write `DispatchQueue.SchedulerTimeType`,
|
||||
/// because Swift is unable to understand which `SchedulerTimeType`
|
||||
/// you're referring to.
|
||||
///
|
||||
/// So you have to write `DispatchQueue.OCombine.SchedulerTimeType`.
|
||||
///
|
||||
/// This bug is tracked [here](https://bugs.swift.org/browse/SR-11183).
|
||||
///
|
||||
/// You can omit this whenever Combine is not available (e. g. on Linux).
|
||||
public struct OCombine: Scheduler {
|
||||
|
||||
public let queue: DispatchQueue
|
||||
|
||||
public init(_ queue: DispatchQueue) {
|
||||
self.queue = queue
|
||||
}
|
||||
|
||||
/// The scheduler time type used by the dispatch queue.
|
||||
public struct SchedulerTimeType: Strideable, Codable, Hashable {
|
||||
|
||||
/// The dispatch time represented by this type.
|
||||
public var dispatchTime: DispatchTime
|
||||
|
||||
/// Creates a dispatch queue time type instance.
|
||||
///
|
||||
/// - Parameter time: The dispatch time to represent.
|
||||
public init(_ time: DispatchTime) {
|
||||
dispatchTime = time
|
||||
}
|
||||
|
||||
/// Returns the distance to another dispatch queue time.
|
||||
///
|
||||
/// - Parameter other: Another dispatch queue time.
|
||||
/// - Returns: The time interval between this time and the provided time.
|
||||
public func distance(to other: SchedulerTimeType) -> Stride {
|
||||
return .nanoseconds(
|
||||
Int(other.dispatchTime.rawValue - dispatchTime.rawValue)
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns a dispatch queue scheduler time calculated by advancing
|
||||
/// this instance’s time by the given interval.
|
||||
///
|
||||
/// - Parameter n: A time interval to advance.
|
||||
/// - Returns: A dispatch queue time advanced by the given
|
||||
/// interval from this instance’s time.
|
||||
public func advanced(by stride: Stride) -> SchedulerTimeType {
|
||||
return .init(dispatchTime + stride.timeInterval)
|
||||
}
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(dispatchTime.rawValue)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
try container.encode(dispatchTime.uptimeNanoseconds)
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
dispatchTime = try .init(uptimeNanoseconds: container.decode(UInt64.self))
|
||||
}
|
||||
|
||||
/// A type that represents the distance between two values.
|
||||
public struct Stride: SchedulerTimeIntervalConvertible,
|
||||
Comparable,
|
||||
SignedNumeric,
|
||||
ExpressibleByFloatLiteral,
|
||||
Hashable,
|
||||
Codable {
|
||||
|
||||
/// If created via floating point literal, the value is
|
||||
/// converted to nanoseconds via multiplication.
|
||||
public typealias FloatLiteralType = Double
|
||||
|
||||
/// Nanoseconds, same as DispatchTimeInterval.
|
||||
public typealias IntegerLiteralType = Int
|
||||
|
||||
/// A type that can represent the absolute value of any possible
|
||||
/// value of the conforming type.
|
||||
public typealias Magnitude = Int
|
||||
|
||||
/// The value of this time interval in nanoseconds.
|
||||
public var magnitude: Int
|
||||
|
||||
/// A `DispatchTimeInterval` created with the value of this type
|
||||
/// in nanoseconds.
|
||||
public var timeInterval: DispatchTimeInterval {
|
||||
return .nanoseconds(magnitude)
|
||||
}
|
||||
|
||||
private init(magnitude: Int) {
|
||||
self.magnitude = magnitude
|
||||
}
|
||||
|
||||
/// Creates a dispatch queue time interval from the given
|
||||
/// dispatch time interval.
|
||||
///
|
||||
/// - Parameter timeInterval: A dispatch time interval.
|
||||
public init(_ timeInterval: DispatchTimeInterval) {
|
||||
switch timeInterval {
|
||||
case .seconds(let seconds):
|
||||
self = .seconds(seconds)
|
||||
case .milliseconds(let milliseconds):
|
||||
self = .milliseconds(milliseconds)
|
||||
case .microseconds(let microseconds):
|
||||
self = .microseconds(microseconds)
|
||||
case .nanoseconds(let nanoseconds):
|
||||
self = .nanoseconds(nanoseconds)
|
||||
case .never:
|
||||
fallthrough
|
||||
@unknown default:
|
||||
self = .nanoseconds(.max)
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a dispatch queue time interval from a floating-point
|
||||
/// seconds value.
|
||||
///
|
||||
/// - Parameter value: The number of seconds, as a `Double`.
|
||||
public init(floatLiteral value: Double) {
|
||||
self = .seconds(value)
|
||||
}
|
||||
|
||||
/// Creates a dispatch queue time interval from an integer seconds value.
|
||||
///
|
||||
/// - Parameter value: The number of seconds, as an `Int`.
|
||||
public init(integerLiteral value: Int) {
|
||||
self = .seconds(value)
|
||||
}
|
||||
|
||||
/// Creates a dispatch queue time interval from a binary integer type.
|
||||
///
|
||||
/// If `exactly` cannot convert to an `Int`, the resulting time interval
|
||||
/// is `nil`.
|
||||
///
|
||||
/// - Parameter exactly: A binary integer representing a time interval.
|
||||
public init?<Source: BinaryInteger>(exactly source: Source) {
|
||||
guard let value = Int(exactly: source) else { return nil }
|
||||
self = .nanoseconds(value)
|
||||
}
|
||||
|
||||
public static func < (lhs: Stride, rhs: Stride) -> Bool {
|
||||
return lhs.magnitude < rhs.magnitude
|
||||
}
|
||||
|
||||
public static func * (lhs: Stride, rhs: Stride) -> Stride {
|
||||
// A bug in Combine, should be nanoseconds (FB7189676)
|
||||
return .seconds(lhs.magnitude * rhs.magnitude)
|
||||
}
|
||||
|
||||
public static func + (lhs: Stride, rhs: Stride) -> Stride {
|
||||
// A bug in Combine, should be nanoseconds (FB7189676)
|
||||
return .seconds(lhs.magnitude + rhs.magnitude)
|
||||
}
|
||||
|
||||
public static func - (lhs: Stride, rhs: Stride) -> Stride {
|
||||
// A bug in Combine, should be nanoseconds (FB7189676)
|
||||
return .seconds(lhs.magnitude - rhs.magnitude)
|
||||
}
|
||||
|
||||
// swiftlint:disable shorthand_operator
|
||||
|
||||
public static func -= (lhs: inout Stride, rhs: Stride) {
|
||||
lhs = lhs - rhs
|
||||
}
|
||||
|
||||
public static func *= (lhs: inout Stride, rhs: Stride) {
|
||||
lhs = lhs * rhs
|
||||
}
|
||||
|
||||
public static func += (lhs: inout Stride, rhs: Stride) {
|
||||
lhs = lhs + rhs
|
||||
}
|
||||
|
||||
// swiftlint:enable shorthand_operator
|
||||
|
||||
public static func seconds(_ value: Double) -> Stride {
|
||||
return Stride(magnitude: Int(value * 1_000_000_000))
|
||||
}
|
||||
|
||||
public static func seconds(_ value: Int) -> Stride {
|
||||
return Stride(magnitude: value * 1_000_000_000)
|
||||
}
|
||||
|
||||
public static func milliseconds(_ value: Int) -> Stride {
|
||||
return Stride(magnitude: value * 1_000_000)
|
||||
}
|
||||
|
||||
public static func microseconds(_ value: Int) -> Stride {
|
||||
return Stride(magnitude: value * 1_000)
|
||||
}
|
||||
|
||||
public static func nanoseconds(_ value: Int) -> Stride {
|
||||
return Stride(magnitude: value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Options that affect the operation of the dispatch queue scheduler.
|
||||
public struct SchedulerOptions {
|
||||
|
||||
/// The dispatch queue quality of service.
|
||||
public var qos: DispatchQoS
|
||||
|
||||
/// The dispatch queue work item flags.
|
||||
public var flags: DispatchWorkItemFlags
|
||||
|
||||
/// The dispatch group, if any, that should be used for performing actions.
|
||||
public var group: DispatchGroup?
|
||||
|
||||
public init(qos: DispatchQoS = .unspecified,
|
||||
flags: DispatchWorkItemFlags = [],
|
||||
group: DispatchGroup? = nil) {
|
||||
self.qos = qos
|
||||
self.flags = flags
|
||||
self.group = group
|
||||
}
|
||||
}
|
||||
|
||||
public var minimumTolerance: SchedulerTimeType.Stride {
|
||||
return .nanoseconds(0)
|
||||
}
|
||||
|
||||
public var now: SchedulerTimeType {
|
||||
return .init(.now())
|
||||
}
|
||||
|
||||
public func schedule(options: SchedulerOptions?,
|
||||
_ action: @escaping () -> Void) {
|
||||
let options = options ?? .init()
|
||||
queue.async(group: options.group,
|
||||
qos: options.qos,
|
||||
flags: options.flags,
|
||||
execute: action)
|
||||
}
|
||||
|
||||
public func schedule(after date: SchedulerTimeType,
|
||||
tolerance: SchedulerTimeType.Stride,
|
||||
options: SchedulerOptions?,
|
||||
_ action: @escaping () -> Void) {
|
||||
let options = options ?? .init()
|
||||
queue.asyncAfter(deadline: date.dispatchTime,
|
||||
qos: options.qos,
|
||||
flags: options.flags,
|
||||
execute: action)
|
||||
}
|
||||
|
||||
/// Performs the action at some time after the specified date, at the specified
|
||||
/// frequency, optionally taking into account tolerance if possible.
|
||||
public func schedule(after date: SchedulerTimeType,
|
||||
interval: SchedulerTimeType.Stride,
|
||||
tolerance: SchedulerTimeType.Stride,
|
||||
options: SchedulerOptions?,
|
||||
_ action: @escaping () -> Void) -> Cancellable {
|
||||
let options = options ?? .init()
|
||||
let timer = DispatchSource.makeTimerSource(queue: queue)
|
||||
timer.setEventHandler(qos: options.qos,
|
||||
flags: options.flags,
|
||||
handler: action)
|
||||
timer.schedule(deadline: date.dispatchTime,
|
||||
repeating: interval.timeInterval,
|
||||
leeway: tolerance.timeInterval)
|
||||
timer.resume()
|
||||
return AnyCancellable(timer.cancel)
|
||||
}
|
||||
}
|
||||
|
||||
/// A namespace for disambiguation when both OpenCombine and Combine are imported.
|
||||
///
|
||||
/// Combine extends `DispatchQueue` with new methods and nested types.
|
||||
/// If you import both OpenCombine and Combine (either explicitly or implicitly,
|
||||
/// e. g. when importing Foundation), you will not be able
|
||||
/// to write `DispatchQueue.main.schedule { doThings() }`,
|
||||
/// because Swift is unable to understand which `schedule` method
|
||||
/// you're referring to.
|
||||
///
|
||||
/// So you have to write `DispatchQueue.main.ocombine.schedule { doThings() }`.
|
||||
///
|
||||
/// This bug is tracked [here](https://bugs.swift.org/browse/SR-11183).
|
||||
///
|
||||
/// You can omit this whenever Combine is not available (e. g. on Linux).
|
||||
public var ocombine: OCombine {
|
||||
return OCombine(self)
|
||||
}
|
||||
}
|
||||
|
||||
#if !canImport(Combine)
|
||||
extension DispatchQueue: OpenCombine.Scheduler {
|
||||
|
||||
public typealias SchedulerOptions = OCombine.SchedulerOptions
|
||||
|
||||
public typealias SchedulerTimeType = OCombine.SchedulerTimeType
|
||||
|
||||
public var minimumTolerance: OCombine.SchedulerTimeType.Stride {
|
||||
return ocombine.minimumTolerance
|
||||
}
|
||||
|
||||
public var now: OCombine.SchedulerTimeType {
|
||||
return ocombine.now
|
||||
}
|
||||
|
||||
public func schedule(options: OCombine.SchedulerOptions?,
|
||||
_ action: @escaping () -> Void) {
|
||||
ocombine.schedule(options: options, action)
|
||||
}
|
||||
|
||||
public func schedule(after date: OCombine.SchedulerTimeType,
|
||||
tolerance: OCombine.SchedulerTimeType.Stride,
|
||||
options: OCombine.SchedulerOptions?,
|
||||
_ action: @escaping () -> Void) {
|
||||
ocombine.schedule(after: date, tolerance: tolerance, options: options, action)
|
||||
}
|
||||
|
||||
public func schedule(after date: OCombine.SchedulerTimeType,
|
||||
interval: OCombine.SchedulerTimeType.Stride,
|
||||
tolerance: OCombine.SchedulerTimeType.Stride,
|
||||
options: OCombine.SchedulerOptions?,
|
||||
_ action: @escaping () -> Void) -> Cancellable {
|
||||
return ocombine.schedule(after: date,
|
||||
interval: interval,
|
||||
tolerance: tolerance,
|
||||
options: options,
|
||||
action)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -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() {
|
||||
@@ -31,7 +26,7 @@ final class AnyPublisherTests: XCTestCase {
|
||||
XCTAssertEqual($0.combineIdentifier, subscriber.combineIdentifier)
|
||||
}
|
||||
)
|
||||
let erased = AnyPublisher(publisher)
|
||||
let erased = publisher.eraseToAnyPublisher()
|
||||
|
||||
erased.subscribe(subscriber)
|
||||
XCTAssertEqual(publisher.history, [.subscriber])
|
||||
|
||||
@@ -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()
|
||||
@@ -50,9 +44,9 @@ final class CombineIdentifierTests: PerformanceTestCase {
|
||||
}
|
||||
|
||||
func testDefaultInitializedPerformance() throws {
|
||||
try benchmark(allowFailure: isDebug, executionCount: 100) {
|
||||
try benchmark(allowFailure: isDebug, executionCount: 500) {
|
||||
for _ in 0..<2000 {
|
||||
_ = CombineIdentifier()
|
||||
blackHole(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(
|
||||
{
|
||||
|
||||
@@ -0,0 +1,461 @@
|
||||
//
|
||||
// DispatchQueueSchedulerTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 26.08.2019.
|
||||
//
|
||||
|
||||
import Dispatch
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
import OpenCombineDispatch
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class DispatchQueueSchedulerTests: XCTestCase {
|
||||
|
||||
// MARK: - Scheduler.SchedulerTimeType
|
||||
|
||||
func testSchedulerTimeTypeDistance() {
|
||||
let time1 = Scheduler.SchedulerTimeType(.init(uptimeNanoseconds: 10000))
|
||||
let time2 = Scheduler.SchedulerTimeType(.init(uptimeNanoseconds: 10431))
|
||||
|
||||
XCTAssertEqual(time1.distance(to: time2), .nanoseconds(431))
|
||||
|
||||
// A bug in Combine (FB7127210), caused by overflow on subtraction.
|
||||
// It should not crash. When they fix it, this test will fail and we'll know
|
||||
// that we need to update our implementation.
|
||||
assertCrashes {
|
||||
_ = time2.distance(to: time1)
|
||||
}
|
||||
}
|
||||
|
||||
func testSchedulerTimeTypeAdvanced() {
|
||||
let time = Scheduler.SchedulerTimeType(.init(uptimeNanoseconds: 10000))
|
||||
let stride1 = Scheduler.SchedulerTimeType.Stride.nanoseconds(431)
|
||||
let stride2 = Scheduler.SchedulerTimeType.Stride.nanoseconds(-220)
|
||||
|
||||
XCTAssertEqual(time.advanced(by: stride1),
|
||||
Scheduler.SchedulerTimeType(.init(uptimeNanoseconds: 10431)))
|
||||
|
||||
XCTAssertEqual(time.advanced(by: stride2),
|
||||
Scheduler.SchedulerTimeType(.init(uptimeNanoseconds: 9780)))
|
||||
}
|
||||
|
||||
func testSchedulerTimeTypeEquatable() {
|
||||
let time1 = Scheduler.SchedulerTimeType(.init(uptimeNanoseconds: 10000))
|
||||
let time2 = Scheduler.SchedulerTimeType(.init(uptimeNanoseconds: 10000))
|
||||
let time3 = Scheduler.SchedulerTimeType(.init(uptimeNanoseconds: 10001))
|
||||
|
||||
XCTAssertEqual(time1, time1)
|
||||
XCTAssertEqual(time2, time2)
|
||||
XCTAssertEqual(time3, time3)
|
||||
|
||||
XCTAssertEqual(time1, time2)
|
||||
XCTAssertEqual(time2, time1)
|
||||
XCTAssertNotEqual(time1, time3)
|
||||
|
||||
assertCrashes {
|
||||
XCTAssertNotEqual(time3, time1)
|
||||
}
|
||||
}
|
||||
|
||||
func testSchedulerTimeTypeHashable() {
|
||||
let time1 = Scheduler.SchedulerTimeType(.init(uptimeNanoseconds: 10000))
|
||||
let time2 = Scheduler.SchedulerTimeType(.init(uptimeNanoseconds: 10001))
|
||||
|
||||
XCTAssertEqual(time1.hashValue, time1.dispatchTime.rawValue.hashValue)
|
||||
XCTAssertEqual(time2.hashValue, time2.dispatchTime.rawValue.hashValue)
|
||||
}
|
||||
|
||||
func testSchedulerTimeTypeCodable() throws {
|
||||
let encoder = JSONEncoder()
|
||||
let decoder = JSONDecoder()
|
||||
|
||||
let time = Scheduler.SchedulerTimeType(.init(uptimeNanoseconds: 42))
|
||||
let encodedData = try encoder
|
||||
.encode(KeyedWrapper(value: time))
|
||||
let encodedString = String(decoding: encodedData, as: UTF8.self)
|
||||
|
||||
XCTAssertEqual(encodedString, #"{"value":42}"#)
|
||||
|
||||
let decodedTime = try decoder
|
||||
.decode(KeyedWrapper<Scheduler.SchedulerTimeType>.self, from: encodedData)
|
||||
.value
|
||||
|
||||
XCTAssertEqual(decodedTime, time)
|
||||
}
|
||||
|
||||
// MARK: - Scheduler.SchedulerTimeType.Stride
|
||||
|
||||
func testStrideToDispatchTimeInterval() {
|
||||
typealias Stride = Scheduler.SchedulerTimeType.Stride
|
||||
|
||||
switch (Stride.seconds(12).timeInterval,
|
||||
Stride.milliseconds(34).timeInterval,
|
||||
Stride.microseconds(56).timeInterval,
|
||||
Stride.nanoseconds(78).timeInterval) {
|
||||
case (.nanoseconds(12000000000),
|
||||
.nanoseconds(34000000),
|
||||
.nanoseconds(56000),
|
||||
.nanoseconds(78)):
|
||||
break // pass
|
||||
case let intervals:
|
||||
XCTFail("Unexpected DispatchTimeInterval: \(intervals)")
|
||||
}
|
||||
}
|
||||
|
||||
func testStrideFromDispatchTimeInterval() {
|
||||
typealias Stride = Scheduler.SchedulerTimeType.Stride
|
||||
|
||||
XCTAssertEqual(Stride(.seconds(12)).magnitude, 12000000000)
|
||||
XCTAssertEqual(Stride(.milliseconds(34)).magnitude, 34000000)
|
||||
XCTAssertEqual(Stride(.microseconds(56)).magnitude, 56000)
|
||||
XCTAssertEqual(Stride(.nanoseconds(78)).magnitude, 78)
|
||||
XCTAssertEqual(Stride(.never).magnitude, .max)
|
||||
}
|
||||
|
||||
func testStrideFromNumericValue() {
|
||||
typealias Stride = Scheduler.SchedulerTimeType.Stride
|
||||
|
||||
XCTAssertEqual(Stride.seconds(12.756).magnitude, 12756000000)
|
||||
XCTAssertEqual(Stride.seconds(34).magnitude, 34000000000)
|
||||
XCTAssertEqual(Stride.milliseconds(56).magnitude, 56000000)
|
||||
XCTAssertEqual(Stride.microseconds(78).magnitude, 78000)
|
||||
XCTAssertEqual(Stride.nanoseconds(90).magnitude, 90)
|
||||
|
||||
XCTAssertEqual((12.756 as Stride).magnitude, 12756000000)
|
||||
XCTAssertEqual((34 as Stride).magnitude, 34000000000)
|
||||
|
||||
XCTAssertNil(Stride(exactly: UInt64.max))
|
||||
XCTAssertEqual(Stride(exactly: 871 as UInt64)?.magnitude, 871)
|
||||
}
|
||||
|
||||
func testStrideComparable() {
|
||||
typealias Stride = Scheduler.SchedulerTimeType.Stride
|
||||
|
||||
XCTAssertLessThan(Stride.nanoseconds(1), .nanoseconds(2))
|
||||
XCTAssertGreaterThan(Stride.nanoseconds(-2), .microseconds(-10))
|
||||
XCTAssertLessThan(Stride.milliseconds(29), .seconds(29))
|
||||
}
|
||||
|
||||
func testStrideMultiplication() {
|
||||
typealias Stride = Scheduler.SchedulerTimeType.Stride
|
||||
|
||||
XCTAssertEqual((Stride.nanoseconds(0) * .nanoseconds(61346)).magnitude, 0)
|
||||
XCTAssertEqual((Stride.nanoseconds(61346) * .nanoseconds(0)).magnitude, 0)
|
||||
XCTAssertEqual((Stride.nanoseconds(18) * .nanoseconds(1)).magnitude,
|
||||
18000000000)
|
||||
XCTAssertEqual((Stride.nanoseconds(18) * .microseconds(1)).magnitude,
|
||||
18000000000000)
|
||||
XCTAssertEqual((Stride.nanoseconds(1) * .nanoseconds(18)).magnitude,
|
||||
18000000000)
|
||||
XCTAssertEqual((Stride.microseconds(1) * .nanoseconds(18)).magnitude,
|
||||
18000000000000)
|
||||
XCTAssertEqual((Stride.nanoseconds(15) * .nanoseconds(2)).magnitude,
|
||||
30000000000)
|
||||
XCTAssertEqual((Stride.microseconds(-3) * .nanoseconds(10)).magnitude,
|
||||
-30000000000000)
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(0)
|
||||
stride *= .nanoseconds(61346)
|
||||
XCTAssertEqual(stride.magnitude, 0)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(61346)
|
||||
stride *= .nanoseconds(0)
|
||||
XCTAssertEqual(stride.magnitude, 0)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(18)
|
||||
stride *= .nanoseconds(1)
|
||||
XCTAssertEqual(stride.magnitude, 18000000000)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(18)
|
||||
stride *= .microseconds(1)
|
||||
XCTAssertEqual(stride.magnitude, 18000000000000)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(1)
|
||||
stride *= .nanoseconds(18)
|
||||
XCTAssertEqual(stride.magnitude, 18000000000)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.microseconds(1)
|
||||
stride *= .nanoseconds(18)
|
||||
XCTAssertEqual(stride.magnitude, 18000000000000)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(15)
|
||||
stride *= .nanoseconds(2)
|
||||
XCTAssertEqual(stride.magnitude, 30000000000)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.microseconds(-3)
|
||||
stride *= .nanoseconds(10)
|
||||
XCTAssertEqual(stride.magnitude, -30000000000000)
|
||||
}
|
||||
}
|
||||
|
||||
func testStrideAddition() {
|
||||
typealias Stride = Scheduler.SchedulerTimeType.Stride
|
||||
|
||||
XCTAssertEqual((Stride.nanoseconds(0) + .microseconds(2)).magnitude,
|
||||
2000000000000)
|
||||
XCTAssertEqual((Stride.nanoseconds(2) + .microseconds(0)).magnitude,
|
||||
2000000000)
|
||||
XCTAssertEqual((Stride.nanoseconds(7) + .nanoseconds(12)).magnitude,
|
||||
19000000000)
|
||||
XCTAssertEqual((Stride.nanoseconds(12) + .nanoseconds(7)).magnitude,
|
||||
19000000000)
|
||||
XCTAssertEqual((Stride.nanoseconds(7) + .nanoseconds(-12)).magnitude,
|
||||
-5000000000)
|
||||
XCTAssertEqual((Stride.nanoseconds(-12) + .nanoseconds(7)).magnitude,
|
||||
-5000000000)
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(0)
|
||||
stride += .microseconds(2)
|
||||
XCTAssertEqual(stride.magnitude, 2000000000000)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(2)
|
||||
stride += .microseconds(0)
|
||||
XCTAssertEqual(stride.magnitude, 2000000000)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(7)
|
||||
stride += .nanoseconds(12)
|
||||
XCTAssertEqual(stride.magnitude, 19000000000)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(12)
|
||||
stride += .nanoseconds(7)
|
||||
XCTAssertEqual(stride.magnitude, 19000000000)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(7)
|
||||
stride += .nanoseconds(-12)
|
||||
XCTAssertEqual(stride.magnitude, -5000000000)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(-12)
|
||||
stride += .nanoseconds(7)
|
||||
XCTAssertEqual(stride.magnitude, -5000000000)
|
||||
}
|
||||
}
|
||||
|
||||
func testStrideSubtraction() {
|
||||
typealias Stride = Scheduler.SchedulerTimeType.Stride
|
||||
|
||||
XCTAssertEqual((Stride.nanoseconds(0) - .microseconds(2)).magnitude,
|
||||
-2000000000000)
|
||||
XCTAssertEqual((Stride.nanoseconds(2) - .microseconds(0)).magnitude,
|
||||
2000000000)
|
||||
XCTAssertEqual((Stride.nanoseconds(7) - .nanoseconds(12)).magnitude,
|
||||
-5000000000)
|
||||
XCTAssertEqual((Stride.nanoseconds(12) - .nanoseconds(7)).magnitude,
|
||||
5000000000)
|
||||
XCTAssertEqual((Stride.nanoseconds(7) - .nanoseconds(-12)).magnitude,
|
||||
19000000000)
|
||||
XCTAssertEqual((Stride.nanoseconds(-12) - .nanoseconds(7)).magnitude,
|
||||
-19000000000)
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(0)
|
||||
stride -= .microseconds(2)
|
||||
XCTAssertEqual(stride.magnitude, -2000000000000)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(2)
|
||||
stride -= .microseconds(0)
|
||||
XCTAssertEqual(stride.magnitude, 2000000000)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(7)
|
||||
stride -= .nanoseconds(12)
|
||||
XCTAssertEqual(stride.magnitude, -5000000000)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(12)
|
||||
stride -= .nanoseconds(7)
|
||||
XCTAssertEqual(stride.magnitude, 5000000000)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(7)
|
||||
stride -= .nanoseconds(-12)
|
||||
XCTAssertEqual(stride.magnitude, 19000000000)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(-12)
|
||||
stride -= .nanoseconds(7)
|
||||
XCTAssertEqual(stride.magnitude, -19000000000)
|
||||
}
|
||||
}
|
||||
|
||||
func testStrideCodable() throws {
|
||||
typealias Stride = Scheduler.SchedulerTimeType.Stride
|
||||
|
||||
let encoder = JSONEncoder()
|
||||
let decoder = JSONDecoder()
|
||||
|
||||
let stride = Stride.nanoseconds(419872)
|
||||
let encodedData = try encoder
|
||||
.encode(KeyedWrapper(value: stride))
|
||||
let encodedString = String(decoding: encodedData, as: UTF8.self)
|
||||
|
||||
XCTAssertEqual(encodedString, #"{"value":{"magnitude":419872}}"#)
|
||||
|
||||
let decodedStride = try decoder
|
||||
.decode(KeyedWrapper<Stride>.self, from: encodedData)
|
||||
.value
|
||||
|
||||
XCTAssertEqual(decodedStride, stride)
|
||||
}
|
||||
|
||||
// MARK: - Scheduler
|
||||
|
||||
func testMinimumTolerance() {
|
||||
XCTAssertEqual(mainScheduler.minimumTolerance, .nanoseconds(0))
|
||||
XCTAssertEqual(backgroundScheduler.minimumTolerance, .nanoseconds(0))
|
||||
}
|
||||
|
||||
func testNow() {
|
||||
let expectedNow = DispatchTime.now().uptimeNanoseconds
|
||||
let actualNowMainScheduler = mainScheduler
|
||||
.now
|
||||
.dispatchTime
|
||||
.uptimeNanoseconds
|
||||
let actualNowBackgroundScheduler = backgroundScheduler
|
||||
.now
|
||||
.dispatchTime
|
||||
.uptimeNanoseconds
|
||||
XCTAssertLessThan(abs(actualNowMainScheduler.distance(to: expectedNow)),
|
||||
1_000_000/*nanoseconds*/)
|
||||
XCTAssertLessThan(abs(actualNowBackgroundScheduler.distance(to: expectedNow)),
|
||||
1_000_000/*nanoseconds*/)
|
||||
}
|
||||
|
||||
func testDefaultSchedulerOptions() {
|
||||
let options = Scheduler.SchedulerOptions()
|
||||
XCTAssertEqual(options.flags, [])
|
||||
XCTAssertEqual(options.qos, .unspecified)
|
||||
XCTAssertNil(options.group)
|
||||
}
|
||||
|
||||
func testScheduleActionOnceNow() {
|
||||
let main = expectation(description: "scheduled on main queue")
|
||||
main.assertForOverFulfill = true
|
||||
|
||||
var didExecuteMainAction = false
|
||||
let didExecuteBackgroundAction = Atomic(false)
|
||||
|
||||
mainScheduler.schedule {
|
||||
didExecuteMainAction = true
|
||||
main.fulfill()
|
||||
}
|
||||
|
||||
let group = DispatchGroup()
|
||||
|
||||
backgroundScheduler
|
||||
.schedule(options: .init(qos: .userInteractive, group: group)) {
|
||||
didExecuteBackgroundAction.do { $0 = true }
|
||||
}
|
||||
|
||||
XCTAssertFalse(didExecuteMainAction, "action should be executed asynchronously")
|
||||
|
||||
// Wait for the background scheduler to execute the work.
|
||||
XCTAssertEqual(group.wait(timeout: .now() + 0.1), .success)
|
||||
|
||||
XCTAssertFalse(didExecuteMainAction, "action should be executed asynchronously")
|
||||
XCTAssertTrue(didExecuteBackgroundAction.value)
|
||||
|
||||
wait(for: [main], timeout: 0.1)
|
||||
}
|
||||
|
||||
func testScheduleActionOnceLater() {
|
||||
|
||||
let main = expectation(description: "scheduled on main queue")
|
||||
main.assertForOverFulfill = true
|
||||
|
||||
var didExecuteAction = false
|
||||
|
||||
let delay = Scheduler.SchedulerTimeType.Stride.milliseconds(200)
|
||||
|
||||
mainScheduler.schedule(after: mainScheduler.now.advanced(by: delay)) {
|
||||
didExecuteAction = true
|
||||
main.fulfill()
|
||||
}
|
||||
|
||||
XCTAssertFalse(didExecuteAction, "action should be executed asynchronously")
|
||||
|
||||
wait(for: [main], timeout: 3/*seconds*/)
|
||||
}
|
||||
|
||||
func testScheduleRepeating() {
|
||||
let main = expectation(description: "scheduled on main queue")
|
||||
main.expectedFulfillmentCount = 4
|
||||
main.assertForOverFulfill = true
|
||||
|
||||
let delay = Scheduler.SchedulerTimeType.Stride.milliseconds(100)
|
||||
let interval = Scheduler.SchedulerTimeType.Stride.milliseconds(50)
|
||||
|
||||
var didExecuteAction = false
|
||||
|
||||
let token = mainScheduler
|
||||
.schedule(after: mainScheduler.now.advanced(by: delay),
|
||||
interval: interval) {
|
||||
didExecuteAction = true
|
||||
main.fulfill()
|
||||
}
|
||||
|
||||
XCTAssert(token is AnyCancellable)
|
||||
XCTAssertFalse(didExecuteAction, "action should be executed asynchronously")
|
||||
|
||||
wait(for: [main], timeout: 3/*seconds*/)
|
||||
}
|
||||
}
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST || !canImport(Combine)
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private typealias Scheduler = DispatchQueue
|
||||
|
||||
private let mainScheduler = DispatchQueue.main
|
||||
private let backgroundScheduler = DispatchQueue.global(qos: .background)
|
||||
|
||||
#else
|
||||
|
||||
private typealias Scheduler = DispatchQueue.OCombine
|
||||
|
||||
private let mainScheduler = DispatchQueue.main.ocombine
|
||||
private let backgroundScheduler = DispatchQueue.global(qos: .background).ocombine
|
||||
|
||||
#endif
|
||||
|
||||
private struct KeyedWrapper<Value: Codable & Equatable>: Codable, Equatable {
|
||||
let value: Value
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -34,13 +34,13 @@ import OpenCombine
|
||||
typealias CustomPublisher = CustomPublisherBase<Int, TestingError>
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class CustomPublisherBase<Output: Equatable, Failure: Error>: Publisher {
|
||||
class CustomPublisherBase<Output: Equatable, Failure: Error>: Publisher {
|
||||
|
||||
private(set) var subscriber: AnySubscriber<Output, Failure>?
|
||||
private(set) var erasedSubscriber: Any?
|
||||
private let subscription: Subscription?
|
||||
|
||||
init(subscription: Subscription?) {
|
||||
required init(subscription: Subscription?) {
|
||||
self.subscription = subscription
|
||||
}
|
||||
|
||||
@@ -60,3 +60,42 @@ final class CustomPublisherBase<Output: Equatable, Failure: Error>: Publisher {
|
||||
subscriber!.receive(completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
typealias CustomConnectablePublisher = CustomConnectablePublisherBase<Int, TestingError>
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class CustomConnectablePublisherBase<Output: Equatable, Failure: Error>
|
||||
: CustomPublisherBase<Output, Failure>,
|
||||
ConnectablePublisher
|
||||
{
|
||||
|
||||
enum Event: CustomStringConvertible {
|
||||
case connected, disconnected
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .connected:
|
||||
return ".connected"
|
||||
case .disconnected:
|
||||
return ".disconnected"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Connection: Cancellable {
|
||||
|
||||
let onCancel: () -> Void
|
||||
|
||||
func cancel() {
|
||||
onCancel()
|
||||
}
|
||||
}
|
||||
|
||||
private(set) var connectionHistory: [Event] = []
|
||||
|
||||
func connect() -> Cancellable {
|
||||
connectionHistory.append(.connected)
|
||||
return Connection { self.connectionHistory.append(.disconnected) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,9 @@ class PerformanceTestCase: GottaGoFast.PerformanceTestCase {
|
||||
block)
|
||||
#endif
|
||||
}
|
||||
|
||||
@inline(never)
|
||||
func blackHole<Value>(_: Value) {}
|
||||
}
|
||||
|
||||
extension XCTestCase {
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
//
|
||||
// TestReflection.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 21/09/2019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
func childrenIsEmpty(_ mirror: Mirror) -> Bool {
|
||||
return mirror.children.isEmpty
|
||||
}
|
||||
|
||||
enum ExpectedMirrorChildValue: Equatable, ExpressibleByStringLiteral {
|
||||
case anything
|
||||
case matches(String)
|
||||
case contains(String)
|
||||
|
||||
typealias StringLiteralType = String
|
||||
|
||||
init(stringLiteral value: String) {
|
||||
self = .matches(value)
|
||||
}
|
||||
}
|
||||
|
||||
func expectedChildren(_ expectedChildren: (String?, ExpectedMirrorChildValue)...,
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line) -> (Mirror) -> Bool {
|
||||
return { mirror in
|
||||
|
||||
let actualChildren = mirror
|
||||
.children
|
||||
.map { ($0, String(describing: $1)) }
|
||||
|
||||
for (actualChild, expectedChild) in zip(actualChildren, expectedChildren) {
|
||||
XCTAssertEqual(actualChild.0, expectedChild.0, file: file, line: line)
|
||||
switch (actualChild.1, expectedChild.1) {
|
||||
case (_, .anything):
|
||||
continue
|
||||
case let (lhs, .matches(rhs)):
|
||||
XCTAssertEqual(lhs, rhs, file: file, line: line)
|
||||
case let (lhs, .contains(rhs)):
|
||||
XCTAssert(lhs.contains(rhs),
|
||||
"\"\(lhs)\" doesn't contain substring \"\(rhs)\"",
|
||||
file: file,
|
||||
line: line)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
internal func testReflection<Output, Failure: Error, Operator: Publisher>(
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line,
|
||||
parentInput: Output.Type,
|
||||
parentFailure: Failure.Type,
|
||||
description expectedDescription: String,
|
||||
customMirror customMirrorPredicate: ((Mirror) -> Bool)?,
|
||||
playgroundDescription: String,
|
||||
_ makeOperator: (CustomConnectablePublisherBase<Output, Failure>) -> Operator
|
||||
) throws where Operator.Output: Equatable {
|
||||
let publisher = CustomConnectablePublisherBase<Output, Failure>(subscription: nil)
|
||||
let operatorPublisher = makeOperator(publisher)
|
||||
let tracking = TrackingSubscriberBase<Operator.Output, Operator.Failure>()
|
||||
operatorPublisher.subscribe(tracking)
|
||||
|
||||
let erasedSubscriber =
|
||||
try XCTUnwrap(publisher.erasedSubscriber, file: file, line: line)
|
||||
|
||||
XCTAssertEqual((erasedSubscriber as? CustomStringConvertible)?.description,
|
||||
expectedDescription,
|
||||
file: file,
|
||||
line: line)
|
||||
|
||||
let customMirror =
|
||||
try XCTUnwrap((erasedSubscriber as? CustomReflectable)?.customMirror,
|
||||
file: file,
|
||||
line: line)
|
||||
|
||||
if let customMirrorPredicate = customMirrorPredicate {
|
||||
XCTAssert(customMirrorPredicate(customMirror),
|
||||
file: file,
|
||||
line: line)
|
||||
}
|
||||
|
||||
XCTAssertEqual(
|
||||
((erasedSubscriber as? CustomPlaygroundDisplayConvertible)?
|
||||
.playgroundDescription as? String),
|
||||
playgroundDescription,
|
||||
file: file,
|
||||
line: line
|
||||
)
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
internal func testSubscriptionReflection<Sut: Publisher>(
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line,
|
||||
description expectedDescription: String,
|
||||
customMirror customMirrorPredicate: ((Mirror) -> Bool)?,
|
||||
playgroundDescription: String,
|
||||
sut: Sut
|
||||
) throws where Sut.Output: Equatable {
|
||||
let tracking = TrackingSubscriberBase<Sut.Output, Sut.Failure>()
|
||||
sut.subscribe(tracking)
|
||||
|
||||
let subscription = try XCTUnwrap(tracking.subscriptions.first?.underlying)
|
||||
|
||||
XCTAssertEqual((subscription as? CustomStringConvertible)?.description,
|
||||
expectedDescription,
|
||||
file: file,
|
||||
line: line)
|
||||
|
||||
if let customMirrorPredicate = customMirrorPredicate {
|
||||
let customMirror =
|
||||
try XCTUnwrap((subscription as? CustomReflectable)?.customMirror,
|
||||
"Subscription doesn't conform to CustomReflectable",
|
||||
file: file,
|
||||
line: line)
|
||||
XCTAssert(customMirrorPredicate(customMirror),
|
||||
file: file,
|
||||
line: line)
|
||||
} else {
|
||||
XCTAssertFalse(subscription is CustomReflectable,
|
||||
"Subscription shouldn't conform to CustomReflectable",
|
||||
file: file,
|
||||
line: line)
|
||||
}
|
||||
|
||||
XCTAssertEqual(
|
||||
((subscription as? CustomPlaygroundDisplayConvertible)?
|
||||
.playgroundDescription as? String),
|
||||
playgroundDescription,
|
||||
file: file,
|
||||
line: line
|
||||
)
|
||||
}
|
||||
@@ -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?()
|
||||
}
|
||||
}
|
||||
@@ -248,8 +263,8 @@ final class TrackingSubjectBase<Output: Equatable, Failure: Error>
|
||||
_passthrough.send(completion: completion)
|
||||
}
|
||||
|
||||
func receive<SubscriberType: Subscriber>(subscriber: SubscriberType)
|
||||
where Failure == SubscriberType.Failure, Output == SubscriberType.Input
|
||||
func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Failure == Downstream.Failure, Output == Downstream.Input
|
||||
{
|
||||
_receiveSubscriber?(subscriber)
|
||||
history.append(.subscriber)
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import XCTest
|
||||
|
||||
// FIXME: Remove this shim as soon is XCTUnwrap is added to swift-corelibs-xctest
|
||||
// FIXME: XCTUnwrap is unavailable in Swift Package Manager yet.
|
||||
|
||||
private struct UnwrappingFailure: Error {}
|
||||
|
||||
|
||||
@@ -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,163 @@
|
||||
//
|
||||
// AutoconnectTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 25/09/2019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class AutoconnectTests: XCTestCase {
|
||||
|
||||
func testBasicRefcountBehavior() throws {
|
||||
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomConnectablePublisher(subscription: subscription)
|
||||
|
||||
let autoconnect = publisher.autoconnect()
|
||||
|
||||
XCTAssertEqual(publisher.connectionHistory, [])
|
||||
|
||||
let subscriber1 = TrackingSubscriber(
|
||||
receiveSubscription: { $0.request(.max(101)) },
|
||||
receiveValue: { _ in .max(201) }
|
||||
)
|
||||
let subscriber2 = TrackingSubscriber(
|
||||
receiveSubscription: { $0.request(.max(102)) },
|
||||
receiveValue: { _ in .max(202) }
|
||||
)
|
||||
let subscriber3 = TrackingSubscriber(
|
||||
receiveSubscription: { $0.request(.max(103)) },
|
||||
receiveValue: { _ in .max(203) }
|
||||
)
|
||||
|
||||
autoconnect.subscribe(subscriber1) // refcount = 1
|
||||
XCTAssertEqual(publisher.connectionHistory, [.connected])
|
||||
|
||||
autoconnect.subscribe(subscriber2) // refcount = 2
|
||||
XCTAssertEqual(publisher.connectionHistory, [.connected])
|
||||
|
||||
autoconnect.subscribe(subscriber3) // refcount = 3
|
||||
XCTAssertEqual(publisher.connectionHistory, [.connected])
|
||||
|
||||
// Autoconnect should just forward events downstream
|
||||
XCTAssertEqual(publisher.send(1), .max(203))
|
||||
XCTAssertEqual(publisher.send(2), .max(203))
|
||||
publisher.send(completion: .finished)
|
||||
XCTAssertEqual(publisher.send(3), .max(203))
|
||||
publisher.send(completion: .failure(.oops))
|
||||
publisher.send(completion: .failure(.oops))
|
||||
|
||||
let subscription1 = try XCTUnwrap(subscriber1.subscriptions.first?.underlying)
|
||||
let subscription2 = try XCTUnwrap(subscriber2.subscriptions.first?.underlying)
|
||||
let subscription3 = try XCTUnwrap(subscriber3.subscriptions.first?.underlying)
|
||||
|
||||
subscription2.cancel() // refcount = 2
|
||||
XCTAssertEqual(publisher.connectionHistory, [.connected])
|
||||
|
||||
subscription3.cancel() // refcount = 1
|
||||
XCTAssertEqual(publisher.connectionHistory, [.connected])
|
||||
|
||||
subscription1.cancel() // refcount = 0
|
||||
XCTAssertEqual(publisher.connectionHistory, [.connected, .disconnected])
|
||||
|
||||
// Cancelling the same subscription twice shouldn't matter
|
||||
subscription1.cancel()
|
||||
XCTAssertEqual(publisher.connectionHistory, [.connected, .disconnected])
|
||||
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(101)),
|
||||
.requested(.max(102)),
|
||||
.requested(.max(103)),
|
||||
.cancelled,
|
||||
.cancelled,
|
||||
.cancelled,
|
||||
.cancelled])
|
||||
|
||||
XCTAssertEqual(subscriber3.history, [.subscription("CustomSubscription"),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.completion(.finished),
|
||||
.value(3),
|
||||
.completion(.failure(.oops)),
|
||||
.completion(.failure(.oops))])
|
||||
}
|
||||
|
||||
func testReentranceWhenConnecting() throws {
|
||||
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomConnectablePublisher(subscription: subscription)
|
||||
|
||||
let autoconnect = publisher.autoconnect()
|
||||
|
||||
let subscriber1 = TrackingSubscriber()
|
||||
|
||||
let subscriber2 = TrackingSubscriber(
|
||||
receiveSubscription: { _ in autoconnect.subscribe(subscriber1) }
|
||||
)
|
||||
|
||||
XCTAssertEqual(publisher.connectionHistory, [])
|
||||
|
||||
autoconnect.subscribe(subscriber2)
|
||||
XCTAssertEqual(publisher.connectionHistory, [.connected,
|
||||
.connected])
|
||||
|
||||
try XCTUnwrap(subscriber2.subscriptions.first?.underlying).cancel()
|
||||
XCTAssertEqual(publisher.connectionHistory, [.connected,
|
||||
.connected,
|
||||
.disconnected])
|
||||
|
||||
try XCTUnwrap(subscriber1.subscriptions.first?.underlying).cancel()
|
||||
XCTAssertEqual(publisher.connectionHistory, [.connected,
|
||||
.connected,
|
||||
.disconnected])
|
||||
}
|
||||
|
||||
func testAutoconnectReflection() throws {
|
||||
|
||||
let customMirrorPredicate = expectedChildren(
|
||||
("parent", .contains("""
|
||||
Publishers.Autoconnect<\
|
||||
OpenCombineTests.\
|
||||
CustomConnectablePublisherBase<Swift.Int, \
|
||||
OpenCombineTests.TestingError>
|
||||
""")),
|
||||
("downstream", "TrackingSubscriberBase<Int, TestingError>: []")
|
||||
)
|
||||
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: TestingError.self,
|
||||
description: "Autoconnect",
|
||||
customMirror: customMirrorPredicate,
|
||||
playgroundDescription: "Autoconnect",
|
||||
{ $0.autoconnect() })
|
||||
|
||||
let subscription = CustomSubscription()
|
||||
let autoconnect = CustomConnectablePublisher(subscription: subscription)
|
||||
.autoconnect()
|
||||
|
||||
try testSubscriptionReflection(
|
||||
description: "CustomSubscription",
|
||||
customMirror: nil,
|
||||
playgroundDescription: "CustomSubscription",
|
||||
sut: autoconnect
|
||||
)
|
||||
|
||||
var autoconnectSubscriptionCombineID: CombineIdentifier?
|
||||
autoconnect.subscribe(
|
||||
TrackingSubscriber(
|
||||
receiveSubscription: {
|
||||
autoconnectSubscriptionCombineID = $0.combineIdentifier
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
XCTAssertEqual(autoconnectSubscriptionCombineID, subscription.combineIdentifier)
|
||||
}
|
||||
}
|
||||
@@ -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,584 @@
|
||||
//
|
||||
// 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.
|
||||
|
||||
private 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 {
|
||||
|
||||
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])
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
@@ -84,22 +33,22 @@ final class JustTests: XCTestCase {
|
||||
.completion(.finished)])
|
||||
}
|
||||
|
||||
func testCustomMirror() throws {
|
||||
let just = Sut(42)
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriberBase<Int, Never>(
|
||||
receiveSubscription: { downstreamSubscription = $0 }
|
||||
)
|
||||
just.subscribe(tracking)
|
||||
func testReflection() throws {
|
||||
try testSubscriptionReflection(description: "Just",
|
||||
customMirror: expectedChildren((nil, "42")),
|
||||
playgroundDescription: "Just",
|
||||
sut: Sut(42))
|
||||
}
|
||||
|
||||
var reflected = ""
|
||||
try dump(XCTUnwrap(downstreamSubscription), to: &reflected)
|
||||
|
||||
XCTAssertEqual(reflected, """
|
||||
▿ Just #0
|
||||
- 42
|
||||
|
||||
""")
|
||||
func testCrashesOnZeroDemand() {
|
||||
assertCrashes {
|
||||
let just = Sut(42)
|
||||
let tracking =
|
||||
TrackingSubscriberBase<Int, Never>(receiveSubscription: {
|
||||
$0.request(.none)
|
||||
})
|
||||
just.subscribe(tracking)
|
||||
}
|
||||
}
|
||||
|
||||
func testJustWithInitialDemand() {
|
||||
@@ -118,13 +67,27 @@ final class JustTests: XCTestCase {
|
||||
func testCancelOnSubscription() {
|
||||
let just = Sut(42)
|
||||
let tracking = TrackingSubscriberBase<Int, Never>(
|
||||
receiveSubscription: { $0.request(.max(1)); $0.cancel() }
|
||||
receiveSubscription: { $0.cancel(); $0.request(.max(1)) }
|
||||
)
|
||||
just.subscribe(tracking)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Just"),
|
||||
.value(42),
|
||||
.completion(.finished)])
|
||||
XCTAssertEqual(tracking.history, [.subscription("Just")])
|
||||
}
|
||||
|
||||
func testRecursion() {
|
||||
let just = Sut(42)
|
||||
var subscription: Subscription?
|
||||
let tracking = TrackingSubscriberBase<Int, Never>(
|
||||
receiveSubscription: {
|
||||
subscription = $0
|
||||
$0.request(.unlimited)
|
||||
},
|
||||
receiveValue: { _ in
|
||||
subscription?.request(.unlimited)
|
||||
return .none
|
||||
}
|
||||
)
|
||||
just.subscribe(tracking)
|
||||
}
|
||||
|
||||
func testLifecycle() {
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
//
|
||||
// MakeConnectableTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 19/09/2019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class MakeConnectableTests: XCTestCase {
|
||||
|
||||
// MakeConnectable is just Multicast that uses PassthroughSubject,
|
||||
// so we can reuse our existing tests
|
||||
|
||||
func testMulticast() throws {
|
||||
try MulticastTests.testGenericMulticast {
|
||||
$0.makeConnectable()
|
||||
}
|
||||
}
|
||||
|
||||
func testMulticastConnectTwice() {
|
||||
MulticastTests.testGenericMulticastConnectTwice {
|
||||
$0.makeConnectable()
|
||||
}
|
||||
}
|
||||
|
||||
func testMulticastDisconnect() {
|
||||
MulticastTests.testGenericMulticastDisconnect {
|
||||
$0.makeConnectable()
|
||||
}
|
||||
}
|
||||
|
||||
func testReflection() throws {
|
||||
try MulticastTests.testGenericMulticastReflection {
|
||||
$0.makeConnectable()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -191,6 +179,15 @@ final class MapErrorTests: XCTestCase {
|
||||
.cancelled])
|
||||
}
|
||||
|
||||
func testMapErrorReflection() throws {
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: Error.self,
|
||||
description: "MapError",
|
||||
customMirror: childrenIsEmpty,
|
||||
playgroundDescription: "MapError",
|
||||
{ $0.mapError { $0 } })
|
||||
}
|
||||
|
||||
func testLifecycle() throws {
|
||||
|
||||
var deinitCounter = 0
|
||||
|
||||
@@ -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
|
||||
@@ -75,11 +52,12 @@ final class MapTests: XCTestCase {
|
||||
func testTryMapFailureBecauseOfThrow() {
|
||||
var counter = 0 // How many times the transform is called?
|
||||
|
||||
let publisher = PassthroughSubject<Int, Error>()
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisherBase<Int, Error>(subscription: subscription)
|
||||
let map = publisher.tryMap { value -> Int in
|
||||
counter += 1
|
||||
if value == 100 {
|
||||
throw "too much" as TestingError
|
||||
throw TestingError.oops
|
||||
}
|
||||
return value * 2
|
||||
}
|
||||
@@ -87,21 +65,28 @@ final class MapTests: XCTestCase {
|
||||
receiveSubscription: { $0.request(.unlimited) }
|
||||
)
|
||||
|
||||
publisher.send(1)
|
||||
XCTAssertEqual(publisher.send(1), .none)
|
||||
map.subscribe(tracking)
|
||||
publisher.send(2)
|
||||
publisher.send(3)
|
||||
publisher.send(100)
|
||||
publisher.send(9)
|
||||
XCTAssertEqual(publisher.send(2), .none)
|
||||
XCTAssertEqual(publisher.send(3), .none)
|
||||
XCTAssertEqual(publisher.send(100), .none)
|
||||
XCTAssertEqual(publisher.send(9), .none)
|
||||
XCTAssertEqual(publisher.send(100), .none)
|
||||
publisher.send(completion: .finished)
|
||||
XCTAssertEqual(publisher.send(100), .none)
|
||||
|
||||
XCTAssertEqual(tracking.history,
|
||||
[.subscription("TryMap"),
|
||||
.value(4),
|
||||
.value(6),
|
||||
.completion(.failure("too much" as TestingError))])
|
||||
.completion(.failure(TestingError.oops)),
|
||||
.value(18),
|
||||
.completion(.failure(TestingError.oops)),
|
||||
.completion(.failure(TestingError.oops))])
|
||||
|
||||
XCTAssertEqual(counter, 3)
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited), .cancelled])
|
||||
|
||||
XCTAssertEqual(counter, 6)
|
||||
}
|
||||
|
||||
func testTryMapFailureOnCompletion() {
|
||||
@@ -234,14 +219,14 @@ 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])
|
||||
}
|
||||
|
||||
func testTryMapCancel() throws {
|
||||
// Given
|
||||
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let map = publisher.tryMap { $0 * 2 }
|
||||
@@ -250,16 +235,17 @@ final class MapTests: XCTestCase {
|
||||
$0.request(.unlimited)
|
||||
downstreamSubscription = $0
|
||||
})
|
||||
// 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])
|
||||
XCTAssertEqual(tracking.history, [.subscription("TryMap"), .value(2)])
|
||||
}
|
||||
|
||||
func testCancelAlreadyCancelled() throws {
|
||||
func testMapCancelAlreadyCancelled() throws {
|
||||
// Given
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
@@ -281,6 +267,73 @@ final class MapTests: XCTestCase {
|
||||
.cancelled])
|
||||
}
|
||||
|
||||
func testTryMapCancelAlreadyCancelled() throws {
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let map = publisher.tryMap { $0 * 2 }
|
||||
let tracking = TrackingSubscriberBase<Int, Error>()
|
||||
map.subscribe(tracking)
|
||||
|
||||
let downstreamSubscription =
|
||||
try XCTUnwrap(tracking.subscriptions.first?.underlying)
|
||||
|
||||
downstreamSubscription.cancel()
|
||||
downstreamSubscription.request(.unlimited)
|
||||
downstreamSubscription.cancel()
|
||||
|
||||
XCTAssertEqual(subscription.history, [.cancelled])
|
||||
}
|
||||
|
||||
func testTryMapReceiveSubscriptionTwice() throws {
|
||||
|
||||
let firstSubscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: firstSubscription)
|
||||
let map = publisher.tryMap { _ -> Int in throw TestingError.oops }
|
||||
let tracking = TrackingSubscriberBase<Int, Error>()
|
||||
map.subscribe(tracking)
|
||||
|
||||
XCTAssertEqual(firstSubscription.history, [])
|
||||
XCTAssertEqual(tracking.history, [.subscription("TryMap")])
|
||||
|
||||
let secondSubscription = CustomSubscription()
|
||||
try XCTUnwrap(publisher.subscriber).receive(subscription: secondSubscription)
|
||||
|
||||
XCTAssertEqual(firstSubscription.history, [])
|
||||
XCTAssertEqual(secondSubscription.history, [.cancelled])
|
||||
XCTAssertEqual(tracking.history, [.subscription("TryMap")])
|
||||
|
||||
XCTAssertEqual(publisher.send(0), .none) // Throws an error
|
||||
|
||||
XCTAssertEqual(firstSubscription.history, [.cancelled])
|
||||
XCTAssertEqual(secondSubscription.history, [.cancelled])
|
||||
XCTAssertEqual(tracking.history, [.subscription("TryMap"),
|
||||
.completion(.failure(TestingError.oops))])
|
||||
try XCTUnwrap(publisher.subscriber).receive(subscription: secondSubscription)
|
||||
|
||||
XCTAssertEqual(firstSubscription.history, [.cancelled])
|
||||
XCTAssertEqual(secondSubscription.history, [.cancelled, .cancelled])
|
||||
XCTAssertEqual(tracking.history, [.subscription("TryMap"),
|
||||
.completion(.failure(TestingError.oops))])
|
||||
}
|
||||
|
||||
func testMapReflection() throws {
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: Never.self,
|
||||
description: "Map",
|
||||
customMirror: childrenIsEmpty,
|
||||
playgroundDescription: "Map",
|
||||
{ $0.map { $0 * 2 } })
|
||||
}
|
||||
|
||||
func testTryMapReflection() throws {
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: Never.self,
|
||||
description: "TryMap",
|
||||
customMirror: childrenIsEmpty,
|
||||
playgroundDescription: "TryMap",
|
||||
{ $0.tryMap { $0 * 2 } })
|
||||
}
|
||||
|
||||
func testLifecycle() throws {
|
||||
|
||||
var deinitCounter = 0
|
||||
@@ -352,12 +405,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,109 +16,20 @@ 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())
|
||||
let multicast = publisher.multicast(PassthroughSubject.init)
|
||||
let tracking = TrackingSubscriber(receiveSubscription: { $0.request(.unlimited) })
|
||||
|
||||
multicast.subscribe(tracking)
|
||||
|
||||
XCTAssertEqual(publisher.send(0), .none)
|
||||
XCTAssertEqual(publisher.send(12), .none)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Multicast")])
|
||||
|
||||
var connection = multicast.connect()
|
||||
|
||||
XCTAssertEqual(publisher.send(-1), .none)
|
||||
XCTAssertEqual(publisher.send(42), .none)
|
||||
|
||||
connection.cancel()
|
||||
|
||||
XCTAssertEqual(publisher.send(14), .none)
|
||||
|
||||
connection = multicast.connect()
|
||||
|
||||
XCTAssertEqual(publisher.send(15), .none)
|
||||
publisher.send(completion: .finished)
|
||||
publisher.send(completion: .finished)
|
||||
|
||||
connection.cancel()
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Multicast"),
|
||||
.value(-1),
|
||||
.value(42),
|
||||
.value(15),
|
||||
.completion(.finished)])
|
||||
try MulticastTests.testGenericMulticast { $0.multicast(PassthroughSubject.init) }
|
||||
}
|
||||
|
||||
func testMulticastConnectTwice() {
|
||||
|
||||
let publisher = TrackingSubject<Int>()
|
||||
let multicastSubject = TrackingSubject<Int>()
|
||||
let multicast = publisher.multicast(subject: multicastSubject)
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: { $0.request(.max(10)) }
|
||||
)
|
||||
|
||||
multicast.subscribe(tracking)
|
||||
|
||||
publisher.send(-1)
|
||||
|
||||
let connection1 = multicast.connect()
|
||||
let connection2 = multicast.connect()
|
||||
|
||||
publisher.send(42)
|
||||
publisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Multicast"),
|
||||
.value(42),
|
||||
.value(42),
|
||||
.completion(.finished)])
|
||||
|
||||
connection1.cancel()
|
||||
connection2.cancel()
|
||||
MulticastTests.testGenericMulticastConnectTwice {
|
||||
$0.multicast { TrackingSubjectBase<Int, Never>() }
|
||||
}
|
||||
}
|
||||
|
||||
func testMulticastDisconnect() {
|
||||
|
||||
let publisher = PassthroughSubject<Int, TestingError>()
|
||||
let multicast = publisher.multicast(PassthroughSubject.init)
|
||||
let tracking = TrackingSubscriber(receiveSubscription: { $0.request(.unlimited) })
|
||||
|
||||
multicast.subscribe(tracking)
|
||||
|
||||
publisher.send(-1)
|
||||
|
||||
var connection = multicast.connect()
|
||||
|
||||
publisher.send(42)
|
||||
connection.cancel()
|
||||
publisher.send(100)
|
||||
|
||||
multicast.subscribe(tracking)
|
||||
connection = multicast.connect()
|
||||
publisher.send(2)
|
||||
publisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Multicast"),
|
||||
.value(42),
|
||||
.subscription("Multicast"),
|
||||
.value(2),
|
||||
.value(2),
|
||||
.completion(.finished),
|
||||
.completion(.finished)])
|
||||
|
||||
connection.cancel()
|
||||
MulticastTests.testGenericMulticastDisconnect {
|
||||
$0.multicast(PassthroughSubject.init)
|
||||
}
|
||||
}
|
||||
|
||||
func testLateSubscriber() {
|
||||
@@ -239,37 +150,9 @@ final class MulticastTests: XCTestCase {
|
||||
|
||||
func testSubscribeAfterCompletion() {
|
||||
|
||||
final class Subj: Subject {
|
||||
|
||||
typealias Output = Int
|
||||
|
||||
typealias Failure = TestingError
|
||||
|
||||
var subscriber: AnySubscriber<Int, TestingError>?
|
||||
|
||||
func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Failure == TestingError, Downstream.Input == Int
|
||||
{
|
||||
subscriber.receive(subscription: CustomSubscription())
|
||||
self.subscriber = AnySubscriber(subscriber)
|
||||
}
|
||||
|
||||
func send(subscription: Subscription) {
|
||||
subscriber?.receive(subscription: subscription)
|
||||
}
|
||||
|
||||
func send(_ value: Int) {
|
||||
_ = subscriber?.receive(value)
|
||||
}
|
||||
|
||||
func send(completion: Subscribers.Completion<TestingError>) {
|
||||
subscriber?.receive(completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
let publisher = CustomPublisher(subscription: CustomSubscription())
|
||||
|
||||
let subject = Subj()
|
||||
let subject = MulticastTestingSubject()
|
||||
|
||||
let multicast = publisher.multicast(subject: subject)
|
||||
|
||||
@@ -285,4 +168,293 @@ final class MulticastTests: XCTestCase {
|
||||
|
||||
XCTAssertEqual(lateSubscriber.history, [.subscription("Multicast")])
|
||||
}
|
||||
|
||||
func testInnerSubscriber() throws {
|
||||
|
||||
let publisher = PassthroughSubject<Int, TestingError>()
|
||||
|
||||
let subject = MulticastTestingSubject()
|
||||
|
||||
let multicast = publisher.multicast(subject: subject)
|
||||
let subscriber = TrackingSubscriberBase<Int, TestingError>(
|
||||
receiveSubscription: { $0.request(.max(42)) },
|
||||
receiveValue: { $0 < 0 ? .unlimited : .max($0) }
|
||||
)
|
||||
|
||||
try withExtendedLifetime(multicast.connect()) {
|
||||
multicast.subscribe(subscriber)
|
||||
|
||||
XCTAssertEqual(subscriber.history, [.subscription("Multicast")])
|
||||
XCTAssertEqual(subject.subscription.history, [.requested(.max(42))])
|
||||
|
||||
// Steal the underlying Multicast subscriber/subscription
|
||||
let innerSubscriber = try XCTUnwrap(subject.subscriber)
|
||||
|
||||
innerSubscriber.receive(subscription: Subscriptions.empty)
|
||||
innerSubscriber.receive(subscription: Subscriptions.empty)
|
||||
|
||||
XCTAssertEqual(subscriber.history,
|
||||
[.subscription("Multicast")],
|
||||
"Downstream subscriber should receive subscription once")
|
||||
XCTAssertEqual(subject.subscription.history, [.requested(.max(42))])
|
||||
|
||||
XCTAssertEqual(innerSubscriber.receive(0), .none)
|
||||
XCTAssertEqual(innerSubscriber.receive(0), .none)
|
||||
XCTAssertEqual(innerSubscriber.receive(1), .none)
|
||||
XCTAssertEqual(innerSubscriber.receive(2), .none)
|
||||
XCTAssertEqual(innerSubscriber.receive(-1), .none)
|
||||
XCTAssertEqual(innerSubscriber.receive(3), .none)
|
||||
XCTAssertEqual(innerSubscriber.receive(-1), .none)
|
||||
XCTAssertEqual(innerSubscriber.receive(0), .none)
|
||||
|
||||
XCTAssertEqual(subscriber.history, [.subscription("Multicast"),
|
||||
.value(0),
|
||||
.value(0),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(-1),
|
||||
.value(3),
|
||||
.value(-1),
|
||||
.value(0)])
|
||||
XCTAssertEqual(subject.subscription.history, [.requested(.max(42)),
|
||||
.requested(.max(1)),
|
||||
.requested(.max(2)),
|
||||
.requested(.unlimited),
|
||||
.requested(.max(3)),
|
||||
.requested(.unlimited)])
|
||||
|
||||
innerSubscriber.receive(completion: .failure(.oops))
|
||||
innerSubscriber.receive(completion: .finished)
|
||||
XCTAssertEqual(innerSubscriber.receive(123), .none)
|
||||
innerSubscriber.receive(subscription: Subscriptions.empty)
|
||||
|
||||
XCTAssertEqual(subscriber.history, [.subscription("Multicast"),
|
||||
.value(0),
|
||||
.value(0),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(-1),
|
||||
.value(3),
|
||||
.value(-1),
|
||||
.value(0),
|
||||
.completion(.failure(.oops))])
|
||||
}
|
||||
}
|
||||
|
||||
func testInnerSubscription() throws {
|
||||
let publisher = PassthroughSubject<Int, TestingError>()
|
||||
|
||||
let subject = MulticastTestingSubject()
|
||||
|
||||
let multicast = publisher.multicast(subject: subject)
|
||||
let subscriber = TrackingSubscriberBase<Int, TestingError>()
|
||||
|
||||
try withExtendedLifetime(multicast.connect()) {
|
||||
multicast.subscribe(subscriber)
|
||||
|
||||
XCTAssertEqual(subscriber.history, [.subscription("Multicast")])
|
||||
XCTAssertEqual(subject.subscription.history, [])
|
||||
|
||||
// Steal the underlying Multicast subscriber/subscription
|
||||
let innerSubscriber = try XCTUnwrap(subject.subscriber)
|
||||
let innerSubscription =
|
||||
try XCTUnwrap(subscriber.subscriptions.first?.underlying)
|
||||
|
||||
innerSubscription.request(.max(42))
|
||||
innerSubscription.request(.max(43))
|
||||
innerSubscription.request(.unlimited)
|
||||
innerSubscription.request(.max(44))
|
||||
|
||||
XCTAssertEqual(subject.subscription.history, [.requested(.max(42)),
|
||||
.requested(.max(43)),
|
||||
.requested(.unlimited),
|
||||
.requested(.max(44))])
|
||||
XCTAssertEqual(subscriber.history, [.subscription("Multicast")])
|
||||
|
||||
innerSubscription.cancel()
|
||||
innerSubscription.cancel()
|
||||
innerSubscription.request(.max(30))
|
||||
innerSubscription.request(.unlimited)
|
||||
innerSubscriber.receive(subscription: Subscriptions.empty)
|
||||
XCTAssertEqual(innerSubscriber.receive(1000), .none)
|
||||
innerSubscriber.receive(completion: .finished)
|
||||
|
||||
XCTAssertEqual(subject.subscription.history, [.requested(.max(42)),
|
||||
.requested(.max(43)),
|
||||
.requested(.unlimited),
|
||||
.requested(.max(44)),
|
||||
.cancelled])
|
||||
XCTAssertEqual(subscriber.history, [.subscription("Multicast")])
|
||||
}
|
||||
}
|
||||
|
||||
func testReflection() throws {
|
||||
try MulticastTests.testGenericMulticastReflection {
|
||||
$0.multicast(PassthroughSubject.init)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Generic tests for Multicast & MakeConnectable
|
||||
|
||||
static func testGenericMulticast<Multicast: ConnectablePublisher>(
|
||||
_ makeMulticast: (CustomPublisherBase<Int, Never>) -> Multicast
|
||||
) throws where Multicast.Output == Int, Multicast.Failure == Never {
|
||||
|
||||
let publisher =
|
||||
CustomPublisherBase<Int, Never>(subscription: CustomSubscription())
|
||||
let multicast = makeMulticast(publisher)
|
||||
let tracking = TrackingSubscriberBase<Int, Never>(
|
||||
receiveSubscription: { $0.request(.unlimited) }
|
||||
)
|
||||
|
||||
multicast.subscribe(tracking)
|
||||
|
||||
XCTAssertEqual(publisher.send(0), .none)
|
||||
XCTAssertEqual(publisher.send(12), .none)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Multicast")])
|
||||
|
||||
var connection = multicast.connect()
|
||||
|
||||
XCTAssertEqual(publisher.send(-1), .none)
|
||||
XCTAssertEqual(publisher.send(42), .none)
|
||||
|
||||
connection.cancel()
|
||||
|
||||
XCTAssertEqual(publisher.send(14), .none)
|
||||
|
||||
connection = multicast.connect()
|
||||
|
||||
XCTAssertEqual(publisher.send(15), .none)
|
||||
publisher.send(completion: .finished)
|
||||
publisher.send(completion: .finished)
|
||||
|
||||
connection.cancel()
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Multicast"),
|
||||
.value(-1),
|
||||
.value(42),
|
||||
.value(15),
|
||||
.completion(.finished)])
|
||||
}
|
||||
|
||||
static func testGenericMulticastConnectTwice<Multicast: ConnectablePublisher>(
|
||||
_ makeMulticast: (TrackingSubjectBase<Int, Never>) -> Multicast
|
||||
) where Multicast.Output == Int, Multicast.Failure == Never {
|
||||
let publisher = TrackingSubjectBase<Int, Never>()
|
||||
let multicast = makeMulticast(publisher)
|
||||
let tracking = TrackingSubscriberBase<Int, Never>(
|
||||
receiveSubscription: { $0.request(.max(10)) }
|
||||
)
|
||||
|
||||
multicast.subscribe(tracking)
|
||||
|
||||
publisher.send(-1)
|
||||
|
||||
let connection1 = multicast.connect()
|
||||
let connection2 = multicast.connect()
|
||||
|
||||
publisher.send(42)
|
||||
publisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Multicast"),
|
||||
.value(42),
|
||||
.value(42),
|
||||
.completion(.finished)])
|
||||
|
||||
connection1.cancel()
|
||||
connection2.cancel()
|
||||
}
|
||||
|
||||
static func testGenericMulticastDisconnect<Multicast: ConnectablePublisher>(
|
||||
_ makeMulticast: (PassthroughSubject<Int, Never>) -> Multicast
|
||||
) where Multicast.Output == Int, Multicast.Failure == Never {
|
||||
|
||||
let publisher = PassthroughSubject<Int, Never>()
|
||||
let multicast = makeMulticast(publisher)
|
||||
let tracking = TrackingSubscriberBase<Int, Never>(
|
||||
receiveSubscription: { $0.request(.unlimited) }
|
||||
)
|
||||
|
||||
multicast.subscribe(tracking)
|
||||
|
||||
publisher.send(-1)
|
||||
|
||||
var connection = multicast.connect()
|
||||
|
||||
publisher.send(42)
|
||||
connection.cancel()
|
||||
publisher.send(100)
|
||||
|
||||
multicast.subscribe(tracking)
|
||||
connection = multicast.connect()
|
||||
publisher.send(2)
|
||||
publisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Multicast"),
|
||||
.value(42),
|
||||
.subscription("Multicast"),
|
||||
.value(2),
|
||||
.value(2),
|
||||
.completion(.finished),
|
||||
.completion(.finished)])
|
||||
|
||||
connection.cancel()
|
||||
}
|
||||
|
||||
static func testGenericMulticastReflection<Multicast: ConnectablePublisher>(
|
||||
_ makeMulticast: (PassthroughSubject<Int, Never>) -> Multicast
|
||||
) throws where Multicast.Output == Int, Multicast.Failure == Never {
|
||||
|
||||
let publisher = PassthroughSubject<Int, Never>()
|
||||
let multicast = makeMulticast(publisher)
|
||||
let tracking = TrackingSubscriberBase<Int, Never>()
|
||||
|
||||
multicast.subscribe(tracking)
|
||||
|
||||
let multicastSubscription =
|
||||
try XCTUnwrap(tracking.subscriptions.first?.underlying)
|
||||
|
||||
let mirror =
|
||||
try XCTUnwrap((multicastSubscription as? CustomReflectable)?.customMirror)
|
||||
|
||||
XCTAssert(mirror.children.isEmpty)
|
||||
|
||||
let playgroundDescription =
|
||||
(multicastSubscription as? CustomPlaygroundDisplayConvertible)?
|
||||
.playgroundDescription as? String
|
||||
|
||||
XCTAssertEqual(playgroundDescription, "Multicast")
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private final class MulticastTestingSubject: Subject {
|
||||
|
||||
typealias Output = Int
|
||||
|
||||
typealias Failure = TestingError
|
||||
|
||||
let subscription = CustomSubscription()
|
||||
|
||||
var subscriber: AnySubscriber<Int, TestingError>?
|
||||
|
||||
func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Failure == TestingError, Downstream.Input == Int
|
||||
{
|
||||
subscriber.receive(subscription: subscription)
|
||||
self.subscriber = AnySubscriber(subscriber)
|
||||
}
|
||||
|
||||
func send(subscription: Subscription) {
|
||||
subscriber?.receive(subscription: subscription)
|
||||
}
|
||||
|
||||
func send(_ value: Int) {
|
||||
_ = subscriber?.receive(value)
|
||||
}
|
||||
|
||||
func send(completion: Subscribers.Completion<TestingError>) {
|
||||
subscriber?.receive(completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -63,9 +23,9 @@ final class OptionalPublisherTests: XCTestCase {
|
||||
#endif
|
||||
|
||||
func testSuccessNoInitialDemand() {
|
||||
let success = Sut(42)
|
||||
let optional = Sut(42)
|
||||
let tracking = TrackingSubscriberBase<Int, Never>()
|
||||
success.subscribe(tracking)
|
||||
optional.subscribe(tracking)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Optional")])
|
||||
|
||||
@@ -78,11 +38,11 @@ final class OptionalPublisherTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testSuccessWithInitialDemand() {
|
||||
let just = Sut(42)
|
||||
let optional = Sut(42)
|
||||
let tracking = TrackingSubscriberBase<Int, Never>(
|
||||
receiveSubscription: { $0.request(.unlimited) }
|
||||
)
|
||||
just.subscribe(tracking)
|
||||
optional.subscribe(tracking)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Optional"),
|
||||
.value(42),
|
||||
@@ -110,6 +70,39 @@ final class OptionalPublisherTests: XCTestCase {
|
||||
.completion(.finished)])
|
||||
}
|
||||
|
||||
func testRecursion() {
|
||||
let optional = Sut(42)
|
||||
var subscription: Subscription?
|
||||
let tracking = TrackingSubscriberBase<Int, Never>(
|
||||
receiveSubscription: {
|
||||
subscription = $0
|
||||
$0.request(.unlimited)
|
||||
},
|
||||
receiveValue: { _ in
|
||||
subscription?.request(.unlimited)
|
||||
return .none
|
||||
}
|
||||
)
|
||||
optional.subscribe(tracking)
|
||||
}
|
||||
|
||||
func testReflection() throws {
|
||||
try testSubscriptionReflection(description: "Optional",
|
||||
customMirror: expectedChildren((nil, "42")),
|
||||
playgroundDescription: "Optional",
|
||||
sut: Sut(42))
|
||||
}
|
||||
|
||||
func testCrashesOnZeroDemand() {
|
||||
assertCrashes {
|
||||
let tracking =
|
||||
TrackingSubscriberBase<Int, Never>(receiveSubscription: {
|
||||
$0.request(.none)
|
||||
})
|
||||
Sut(42).subscribe(tracking)
|
||||
}
|
||||
}
|
||||
|
||||
func testLifecycle() {
|
||||
var deinitCount = 0
|
||||
do {
|
||||
@@ -194,7 +187,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,15 +16,9 @@ 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()
|
||||
let stream = HistoryStream()
|
||||
let subscription = CustomSubscription(
|
||||
onRequest: { _ in stream.write("callback request demand\n") },
|
||||
onCancel: { stream.write("callback cancel subscription\n") }
|
||||
@@ -74,44 +68,81 @@ final class PrintTests: XCTestCase {
|
||||
.requested(.max(30)),
|
||||
.cancelled])
|
||||
|
||||
let expectedOutput = """
|
||||
receive subscription: (CustomSubscription)
|
||||
callback subscription
|
||||
request unlimited
|
||||
callback request demand
|
||||
request max: (30)
|
||||
callback request demand
|
||||
receive cancel
|
||||
callback cancel subscription
|
||||
receive value: (1)
|
||||
callback value
|
||||
request max: (100)
|
||||
request max: (2) (synchronous)
|
||||
request max: (42)
|
||||
receive value: (2)
|
||||
callback value
|
||||
request max: (100)
|
||||
request max: (2) (synchronous)
|
||||
receive finished
|
||||
callback completion
|
||||
request max: (12)
|
||||
receive error: (failure)
|
||||
callback completion
|
||||
request max: (12)
|
||||
receive value: (10)
|
||||
callback value
|
||||
request max: (100)
|
||||
request unlimited (synchronous)
|
||||
receive cancel
|
||||
|
||||
"""
|
||||
let expectedOutput = [
|
||||
"",
|
||||
"receive subscription: (CustomSubscription)",
|
||||
"\n",
|
||||
"callback subscription\n",
|
||||
"",
|
||||
"request unlimited",
|
||||
"\n",
|
||||
"callback request demand\n",
|
||||
"",
|
||||
"request max: (30)",
|
||||
"\n",
|
||||
"callback request demand\n",
|
||||
"",
|
||||
"receive cancel",
|
||||
"\n",
|
||||
"callback cancel subscription\n",
|
||||
"",
|
||||
"receive value: (1)",
|
||||
"\n",
|
||||
"callback value\n",
|
||||
"",
|
||||
"request max: (100)",
|
||||
"\n",
|
||||
"",
|
||||
"request max: (2) (synchronous)",
|
||||
"\n",
|
||||
"",
|
||||
"request max: (42)",
|
||||
"\n",
|
||||
"",
|
||||
"receive value: (2)",
|
||||
"\n",
|
||||
"callback value\n",
|
||||
"",
|
||||
"request max: (100)",
|
||||
"\n",
|
||||
"",
|
||||
"request max: (2) (synchronous)",
|
||||
"\n",
|
||||
"",
|
||||
"receive finished",
|
||||
"\n",
|
||||
"callback completion\n",
|
||||
"",
|
||||
"request max: (12)",
|
||||
"\n",
|
||||
"",
|
||||
"receive error: (failure)",
|
||||
"\n",
|
||||
"callback completion\n",
|
||||
"",
|
||||
"request max: (12)",
|
||||
"\n",
|
||||
"",
|
||||
"receive value: (10)",
|
||||
"\n",
|
||||
"callback value\n",
|
||||
"",
|
||||
"request max: (100)",
|
||||
"\n",
|
||||
"",
|
||||
"request unlimited (synchronous)",
|
||||
"\n",
|
||||
"",
|
||||
"receive cancel",
|
||||
"\n"
|
||||
]
|
||||
|
||||
XCTAssertEqual(stream.output.value, expectedOutput)
|
||||
}
|
||||
|
||||
func testPrintWithPrefix() {
|
||||
|
||||
let stream = StringStream()
|
||||
let stream = HistoryStream()
|
||||
let subscription = CustomSubscription(
|
||||
onRequest: { _ in stream.write("callback request demand\n") },
|
||||
onCancel: { stream.write("callback cancel subscription\n") }
|
||||
@@ -161,44 +192,81 @@ final class PrintTests: XCTestCase {
|
||||
.requested(.max(30)),
|
||||
.cancelled])
|
||||
|
||||
let expectedOutput = """
|
||||
👉: receive subscription: (CustomSubscription)
|
||||
callback subscription
|
||||
👉: request unlimited
|
||||
callback request demand
|
||||
👉: request max: (30)
|
||||
callback request demand
|
||||
👉: receive cancel
|
||||
callback cancel subscription
|
||||
👉: receive value: (1)
|
||||
callback value
|
||||
👉: request max: (100)
|
||||
👉: request max: (2) (synchronous)
|
||||
👉: request max: (42)
|
||||
👉: receive value: (2)
|
||||
callback value
|
||||
👉: request max: (100)
|
||||
👉: request max: (2) (synchronous)
|
||||
👉: receive finished
|
||||
callback completion
|
||||
👉: request max: (12)
|
||||
👉: receive error: (failure)
|
||||
callback completion
|
||||
👉: request max: (12)
|
||||
👉: receive value: (10)
|
||||
callback value
|
||||
👉: request max: (100)
|
||||
👉: request unlimited (synchronous)
|
||||
👉: receive cancel
|
||||
|
||||
"""
|
||||
let expectedOutput = [
|
||||
"",
|
||||
"👉: receive subscription: (CustomSubscription)",
|
||||
"\n",
|
||||
"callback subscription\n",
|
||||
"",
|
||||
"👉: request unlimited",
|
||||
"\n",
|
||||
"callback request demand\n",
|
||||
"",
|
||||
"👉: request max: (30)",
|
||||
"\n",
|
||||
"callback request demand\n",
|
||||
"",
|
||||
"👉: receive cancel",
|
||||
"\n",
|
||||
"callback cancel subscription\n",
|
||||
"",
|
||||
"👉: receive value: (1)",
|
||||
"\n",
|
||||
"callback value\n",
|
||||
"",
|
||||
"👉: request max: (100)",
|
||||
"\n",
|
||||
"",
|
||||
"👉: request max: (2) (synchronous)",
|
||||
"\n",
|
||||
"",
|
||||
"👉: request max: (42)",
|
||||
"\n",
|
||||
"",
|
||||
"👉: receive value: (2)",
|
||||
"\n",
|
||||
"callback value\n",
|
||||
"",
|
||||
"👉: request max: (100)",
|
||||
"\n",
|
||||
"",
|
||||
"👉: request max: (2) (synchronous)",
|
||||
"\n",
|
||||
"",
|
||||
"👉: receive finished",
|
||||
"\n",
|
||||
"callback completion\n",
|
||||
"",
|
||||
"👉: request max: (12)",
|
||||
"\n",
|
||||
"",
|
||||
"👉: receive error: (failure)",
|
||||
"\n",
|
||||
"callback completion\n",
|
||||
"",
|
||||
"👉: request max: (12)",
|
||||
"\n",
|
||||
"",
|
||||
"👉: receive value: (10)",
|
||||
"\n",
|
||||
"callback value\n",
|
||||
"",
|
||||
"👉: request max: (100)",
|
||||
"\n",
|
||||
"",
|
||||
"👉: request unlimited (synchronous)",
|
||||
"\n",
|
||||
"",
|
||||
"👉: receive cancel",
|
||||
"\n"
|
||||
]
|
||||
|
||||
XCTAssertEqual(stream.output.value, expectedOutput)
|
||||
}
|
||||
|
||||
func testSynchronization() {
|
||||
|
||||
let stream = StringStream()
|
||||
let stream = HistoryStream()
|
||||
let publisher = CustomPublisherBase<Int, Never>(subscription: nil)
|
||||
let printer = publisher.print(to: stream)
|
||||
|
||||
@@ -214,11 +282,11 @@ final class PrintTests: XCTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
private final class StringStream: TextOutputStream {
|
||||
private final class HistoryStream: TextOutputStream {
|
||||
|
||||
var output = Atomic("")
|
||||
let output = Atomic([String]())
|
||||
|
||||
func write(_ string: String) {
|
||||
output.do { $0.write(string) }
|
||||
output.do { $0.append(string) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
//
|
||||
// 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)])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(-1), .none)
|
||||
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
|
||||
XCTAssertEqual(helper.publisher.send(9), .none)
|
||||
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)])
|
||||
}
|
||||
|
||||
func testCrashOnReceiveValueWithZeroPendingDemand() {
|
||||
let publisher = CustomPublisher(subscription: CustomSubscription())
|
||||
let replaceError = publisher.replaceError(with: 0)
|
||||
let tracking = TrackingSubscriberBase<Int, Never>()
|
||||
replaceError.subscribe(tracking)
|
||||
|
||||
assertCrashes {
|
||||
XCTAssertEqual(publisher.send(1), .none)
|
||||
}
|
||||
}
|
||||
|
||||
func testReplaceErrorReflection() throws {
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: Error.self,
|
||||
description: "ReplaceError",
|
||||
customMirror: childrenIsEmpty,
|
||||
playgroundDescription: "ReplaceError",
|
||||
{ $0.replaceError(with: 0) })
|
||||
}
|
||||
|
||||
func testReceiveSubscriptionTwice() {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(1),
|
||||
receiveValueDemand: .none,
|
||||
createSut: { $0.replaceError(with: 42) })
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(1))])
|
||||
|
||||
let anotherSubscription = CustomSubscription()
|
||||
helper.publisher.subscriber?.receive(subscription: anotherSubscription)
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(1))])
|
||||
XCTAssertEqual(anotherSubscription.history, [.cancelled])
|
||||
|
||||
helper.downstreamSubscription?.cancel()
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(1)), .cancelled])
|
||||
|
||||
helper.publisher.subscriber?.receive(subscription: anotherSubscription)
|
||||
|
||||
XCTAssertEqual(anotherSubscription.history, [.cancelled, .cancelled])
|
||||
}
|
||||
|
||||
func testReceiveCompletionTwice() {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(1),
|
||||
receiveValueDemand: .none,
|
||||
createSut: { $0.replaceError(with: 42) })
|
||||
|
||||
helper.publisher.send(completion: .finished)
|
||||
helper.publisher.send(completion: .finished)
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("ReplaceError"),
|
||||
.completion(.finished),
|
||||
.completion(.finished)])
|
||||
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
XCTAssertEqual(helper.publisher.send(-1), .none)
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("ReplaceError"),
|
||||
.completion(.finished),
|
||||
.completion(.finished),
|
||||
.value(42),
|
||||
.completion(.finished),
|
||||
.value(42),
|
||||
.completion(.finished),
|
||||
.value(-1)])
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -91,4 +86,13 @@ final class ReplaceNilTests: XCTestCase {
|
||||
.value(42),
|
||||
.completion(.finished)])
|
||||
}
|
||||
|
||||
func testReplaceNilReflection() throws {
|
||||
try testReflection(parentInput: Int?.self,
|
||||
parentFailure: Never.self,
|
||||
description: "Map",
|
||||
customMirror: childrenIsEmpty,
|
||||
playgroundDescription: "Map",
|
||||
{ $0.replaceNil(with: 0) })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -143,6 +100,46 @@ final class ResultPublisherTests: XCTestCase {
|
||||
.completion(.failure("failure"))])
|
||||
}
|
||||
|
||||
func testRecursion() {
|
||||
let success = makePublisher(42)
|
||||
var subscription: Subscription?
|
||||
let tracking = TrackingSubscriberBase<Int, TestingError>(
|
||||
receiveSubscription: {
|
||||
subscription = $0
|
||||
$0.request(.unlimited)
|
||||
},
|
||||
receiveValue: { _ in
|
||||
subscription?.request(.unlimited)
|
||||
return .none
|
||||
}
|
||||
)
|
||||
success.subscribe(tracking)
|
||||
}
|
||||
|
||||
func testReflection() throws {
|
||||
|
||||
func testCustomMirror(_ mirror: Mirror) -> Bool {
|
||||
return mirror.children.count == 1 &&
|
||||
mirror.children.first!.label == nil &&
|
||||
(mirror.children.first!.value as? Int) == 42
|
||||
}
|
||||
|
||||
try testSubscriptionReflection(description: "Once",
|
||||
customMirror: expectedChildren((nil, "42")),
|
||||
playgroundDescription: "Once",
|
||||
sut: Sut(42))
|
||||
}
|
||||
|
||||
func testCrashesOnZeroDemand() {
|
||||
assertCrashes {
|
||||
let tracking =
|
||||
TrackingSubscriberBase<Int, TestingError>(receiveSubscription: {
|
||||
$0.request(.none)
|
||||
})
|
||||
makePublisher(42).subscribe(tracking)
|
||||
}
|
||||
}
|
||||
|
||||
func testLifecycle() {
|
||||
var deinitCount = 0
|
||||
do {
|
||||
|
||||
@@ -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
|
||||
@@ -220,6 +166,75 @@ final class SequenceTests: XCTestCase {
|
||||
.value(1)])
|
||||
}
|
||||
|
||||
func testCancelOnValue() {
|
||||
let counter = Counter(upperBound: 3)
|
||||
let publisher = makePublisher(counter)
|
||||
var subscription: Subscription?
|
||||
let subscriber = TrackingSubscriberBase<Int, Never>(
|
||||
receiveSubscription: {
|
||||
subscription = $0
|
||||
$0.request(.unlimited)
|
||||
},
|
||||
receiveValue: { _ in
|
||||
subscription?.cancel()
|
||||
return .unlimited
|
||||
}
|
||||
)
|
||||
publisher.subscribe(subscriber)
|
||||
|
||||
XCTAssertEqual(subscriber.history, [.subscription("Sequence"),
|
||||
.value(1)])
|
||||
|
||||
subscriber.subscriptions.first?.request(.max(1))
|
||||
|
||||
XCTAssertEqual(subscriber.history, [.subscription("Sequence"),
|
||||
.value(1)])
|
||||
}
|
||||
|
||||
func testPublishesCorrectValues() {
|
||||
let sequence = makePublisher(1...5)
|
||||
|
||||
var history = [Int]()
|
||||
_ = sequence.sink {
|
||||
history.append($0)
|
||||
}
|
||||
|
||||
XCTAssertEqual(history, [1, 2, 3, 4, 5])
|
||||
}
|
||||
|
||||
func testRecursion() {
|
||||
let sequence = makePublisher(1...5)
|
||||
|
||||
var history = [Int]()
|
||||
var storedSubscription: Subscription?
|
||||
|
||||
let tracking = TrackingSubscriberBase<Int, Never>(
|
||||
receiveSubscription: { subscription in
|
||||
storedSubscription = subscription
|
||||
subscription.request(.none) // Shouldn't crash
|
||||
subscription.request(.max(1))
|
||||
},
|
||||
receiveValue: { value in
|
||||
storedSubscription?.request(.max(1))
|
||||
history.append(value)
|
||||
return .none
|
||||
}
|
||||
)
|
||||
|
||||
sequence.subscribe(tracking)
|
||||
|
||||
XCTAssertEqual(history, [1, 2, 3, 4, 5])
|
||||
}
|
||||
|
||||
func testReflection() throws {
|
||||
|
||||
try testSubscriptionReflection(description: "1...5",
|
||||
customMirror:
|
||||
expectedChildren(("sequence", "1...5")),
|
||||
playgroundDescription: "1...5",
|
||||
sut: makePublisher(1...5))
|
||||
}
|
||||
|
||||
func testLifecycle() throws {
|
||||
|
||||
var deinitCounter = 0
|
||||
|
||||
@@ -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) }
|
||||
@@ -169,4 +158,13 @@ final class SetFailureTypeTests: XCTestCase {
|
||||
.requested(.unlimited),
|
||||
.cancelled])
|
||||
}
|
||||
|
||||
func testSetFailureTypeReflection() throws {
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: Never.self,
|
||||
description: "SetFailureType",
|
||||
customMirror: childrenIsEmpty,
|
||||
playgroundDescription: "SetFailureType",
|
||||
{ $0.setFailureType(to: Error.self) })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
//
|
||||
// ShareTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 03/10/2019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class ShareTests: XCTestCase {
|
||||
|
||||
func testBasicBehavior() {
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
|
||||
let share = publisher.share()
|
||||
|
||||
XCTAssertEqual(subscription.history, [])
|
||||
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: { $0.request(.max(42)) },
|
||||
receiveValue: { .max($0) }
|
||||
)
|
||||
|
||||
share.subscribe(tracking)
|
||||
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(tracking.history, [.subscription("Multicast")])
|
||||
|
||||
XCTAssertEqual(publisher.send(1), .none)
|
||||
XCTAssertEqual(publisher.send(2), .none)
|
||||
XCTAssertEqual(publisher.send(3), .none)
|
||||
publisher.send(completion: .failure(.oops))
|
||||
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(tracking.history, [.subscription("Multicast"),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3),
|
||||
.completion(.failure(.oops))])
|
||||
}
|
||||
|
||||
func testLateSubscriber() {
|
||||
let subscription = CustomSubscription()
|
||||
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let share = publisher.share()
|
||||
|
||||
let earlySubscriber = TrackingSubscriber(
|
||||
receiveSubscription: {
|
||||
$0.request(.max(3))
|
||||
}
|
||||
)
|
||||
|
||||
share.subscribe(earlySubscriber)
|
||||
|
||||
XCTAssertNotNil(publisher.subscriber)
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(earlySubscriber.history, [.subscription("Multicast")])
|
||||
|
||||
XCTAssertEqual(publisher.send(1), .none)
|
||||
XCTAssertEqual(publisher.send(2), .none)
|
||||
XCTAssertEqual(publisher.send(3), .none)
|
||||
XCTAssertEqual(publisher.send(4), .none)
|
||||
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(earlySubscriber.history, [.subscription("Multicast"),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3)])
|
||||
|
||||
let lateSubscriber = TrackingSubscriber(
|
||||
receiveSubscription: {
|
||||
$0.request(.max(2))
|
||||
}
|
||||
)
|
||||
|
||||
share.subscribe(lateSubscriber)
|
||||
|
||||
XCTAssertEqual(publisher.send(5), .none)
|
||||
XCTAssertEqual(publisher.send(6), .none)
|
||||
XCTAssertEqual(publisher.send(7), .none)
|
||||
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(earlySubscriber.history, [.subscription("Multicast"),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3)])
|
||||
XCTAssertEqual(lateSubscriber.history, [.subscription("Multicast"),
|
||||
.value(5),
|
||||
.value(6)])
|
||||
|
||||
publisher.send(completion: .finished)
|
||||
|
||||
let latestSubscriber = TrackingSubscriber(
|
||||
receiveSubscription: {
|
||||
$0.request(.none)
|
||||
}
|
||||
)
|
||||
|
||||
share.subscribe(latestSubscriber)
|
||||
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(earlySubscriber.history, [.subscription("Multicast"),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3),
|
||||
.completion(.finished)])
|
||||
XCTAssertEqual(lateSubscriber.history, [.subscription("Multicast"),
|
||||
.value(5),
|
||||
.value(6),
|
||||
.completion(.finished)])
|
||||
XCTAssertEqual(latestSubscriber.history, [.subscription("Multicast"),
|
||||
.completion(.finished)])
|
||||
}
|
||||
|
||||
func testEquatable() {
|
||||
|
||||
let publisher = CustomPublisher(subscription: nil)
|
||||
let share1 = publisher.share()
|
||||
let share2 = publisher.share()
|
||||
|
||||
XCTAssertEqual(share1, share1)
|
||||
XCTAssertEqual(share2, share2)
|
||||
XCTAssertNotEqual(share1, share2)
|
||||
XCTAssertNotEqual(share2, share1)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -69,7 +61,7 @@ final class AssignTests: XCTestCase {
|
||||
|
||||
let subscription1 = CustomSubscription()
|
||||
assign.receive(subscription: subscription1)
|
||||
XCTAssertEqual(subscription1.lastRequested, .unlimited)
|
||||
XCTAssertEqual(subscription1.history, [.requested(.unlimited)])
|
||||
XCTAssertFalse(subscription1.cancelled)
|
||||
|
||||
let subscription2 = CustomSubscription()
|
||||
@@ -83,6 +75,11 @@ final class AssignTests: XCTestCase {
|
||||
subscription1.cancelled = false
|
||||
assign.receive(completion: .finished)
|
||||
XCTAssertTrue(subscription1.cancelled)
|
||||
|
||||
let subscription3 = CustomSubscription()
|
||||
assign.receive(subscription: subscription3)
|
||||
XCTAssertEqual(subscription3.history, [.cancelled])
|
||||
XCTAssertTrue(subscription3.cancelled)
|
||||
}
|
||||
|
||||
func testReceiveValue() {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user