69 Commits

Author SHA1 Message Date
Sergej Jaskiewicz 5f92ee05d2 Fix the semantics to be compatible with Xcode 11.1 (#74) 2019-10-08 11:20:42 +03:00
Sven bdd703abb3 Fix Sequence cancellation (#73)
* Add test to cancel after first value received from Sequence publisher

* Stop sending values from sequence if cancelled
2019-10-07 23:19:05 +03:00
Sergej Jaskiewicz e41c48a5cd Use UInt64 as CombineIdentifier (instead of UInt) 2019-10-03 15:37:44 +03:00
Sergej Jaskiewicz df0b8b08db Add tests for Publishers.Share 2019-10-03 15:22:18 +03:00
Sergej Jaskiewicz 7056143b99 Add tests for Publishers.Autoconnect 2019-10-03 15:22:18 +03:00
Sergej Jaskiewicz 0a965ba60a Adopt new locking API 2019-10-03 15:22:18 +03:00
Sergej Jaskiewicz 7dfaa4edea Implement Publishers.Share 2019-10-03 15:22:18 +03:00
Sergej Jaskiewicz 3e8f2774a4 Implement Publishers.Autoconnect 2019-10-03 15:22:18 +03:00
Sergej Jaskiewicz 68e9bbe164 Extract generation of a next CombineIdentifier to COpenCombineHelpers (#69) 2019-10-02 23:28:09 +03:00
Sergej Jaskiewicz 0f71c33d72 Add header guards 2019-09-23 16:21:08 +03:00
Sergej Jaskiewicz 3f61648f82 Use CInt instead of Int32 2019-09-23 16:21:08 +03:00
Sergej Jaskiewicz c621ceb267 Rename OpenCombineAtomics -> COpenCombineAtomics 2019-09-23 16:21:08 +03:00
Sergej Jaskiewicz 2aa297ec39 Fix access race detected by TSan 2019-09-23 16:21:08 +03:00
Sergej Jaskiewicz 9cb27bb91b Better Locking internal API 2019-09-23 16:21:08 +03:00
Sergej Jaskiewicz d74f68da86 Audit Subscribers.Assign for thread safety (nothing to do here) 2019-09-22 23:10:54 +03:00
Sergej Jaskiewicz f68dcd520f Audit Subscribers.Sink for thread safety (nothing to do here) 2019-09-22 23:10:54 +03:00
Sergej Jaskiewicz 432fd4f48f Audit Publishers.ReplaceError for thread safety 2019-09-22 23:10:54 +03:00
Sergej Jaskiewicz 9c6bbda0c4 Make Publishers.MapError.Inner a struct 2019-09-22 23:10:54 +03:00
Sergej Jaskiewicz 3990ec2afb Audit Publishers.Sequence for thread safety 2019-09-22 23:10:54 +03:00
Sergej Jaskiewicz 39dd9e40bf Add reflection test for Publishers.ReplaceNil 2019-09-22 23:10:54 +03:00
Sergej Jaskiewicz fd7c0459b9 Make Publishers.SetFailureType.Inner a struct 2019-09-22 23:10:54 +03:00
Sergej Jaskiewicz f7145e7fa5 Fix TryMap compatibility tests 2019-09-22 23:10:54 +03:00
Sergej Jaskiewicz ecd4766129 Audit Optional.Publisher for thread safety (nothing to do here) 2019-09-22 23:10:54 +03:00
Sergej Jaskiewicz e00a6f06fc Audit Optional.Publisher for thread safety (nothing to do here) 2019-09-22 23:10:54 +03:00
Sergej Jaskiewicz 23ee3a4b7b Audit Just for thread safety (nothing to do there) 2019-09-22 23:10:54 +03:00
Sergej Jaskiewicz 9c913124eb Audit TryMap for thread safety, fix its semantics (#64) 2019-09-21 22:09:48 +03:00
Sergej Jaskiewicz 7ddd15b334 Audit Publishers.Multicast for thread safety (#63) 2019-09-20 16:15:35 +03:00
Sergej Jaskiewicz 72753ef93c Implement Publishers.MakeConnectable (#61)
* Implement Publishers.MakeConnectable

* Add MakeConnectable tests
2019-09-19 14:06:57 +03:00
Sergej Jaskiewicz 816426b48c Fix iterator.next() being called twice in Publishers.Sequence 2019-09-19 05:10:34 +03:00
Sergej Jaskiewicz 47fb390081 Add eraseToAnyPublisher() method 2019-09-18 16:55:48 +03:00
Sergej Jaskiewicz 1d3327f6bf Revert "Remove XCTUnwrap implementation"
This reverts commit 16690e6f1f.
2019-09-16 19:13:24 +03:00
Sergej Jaskiewicz eb7478d430 Add Unreachable optimizer hint 2019-09-16 19:13:24 +03:00
Sergej Jaskiewicz f69621f0e2 Remove XCTUnwrap implementation 2019-09-16 19:13:24 +03:00
Sergej Jaskiewicz 7f3cccf1ae Audit SubjectSubscriber for thread-safety 2019-09-16 19:13:24 +03:00
Sergej Jaskiewicz ec037dbb3d Fix semantics of Publishers.Print 2019-09-16 19:13:24 +03:00
Sergej Jaskiewicz 8a39f35d3f Simplify Publishers.Multicast.connect() method 2019-09-16 19:13:24 +03:00
Sergej Jaskiewicz 7fb92bffc6 Replace SubscriberType with Downstream in generic params 2019-09-16 19:13:24 +03:00
Sergej Jaskiewicz e441ea3048 Add missing Equatable conformances for First, ReplaceError 2019-09-16 19:13:24 +03:00
Sergej Jaskiewicz 22f7b6d10d Fix code style issues with FlatMap 2019-09-16 19:13:24 +03:00
Sergej Jaskiewicz 7431d21c9c Increase timeouts for DispatchQueueSchedulerTests 2019-09-15 16:07:52 +03:00
Sergej Jaskiewicz 1d901fca7f Remove flaky test for DispatchQueue scheduler 2019-09-15 16:07:52 +03:00
Sergej Jaskiewicz 9834eab0ea Remove some nasty unsafe code from tests
https://twitter.com/UINT_MIN/status/1168581618753163264
2019-09-15 16:07:52 +03:00
Sergej Jaskiewicz 1ce9660ce9 Remove .swiftpm 2019-09-15 16:07:52 +03:00
Sergej Jaskiewicz 313d6befa6 Fix a data race in DispatchQueueSchedulerTests
Also gitignore .swiftpm and make SwiftLint happy
2019-09-15 16:07:52 +03:00
Sergej Jaskiewicz 8c7f061892 Fully cover DispatchQueue extension with tests 2019-09-15 16:07:52 +03:00
Sergej Jaskiewicz 2ac2470579 Initial implementation of DispatchQueue scheduler 2019-09-15 16:07:52 +03:00
Sergej Jaskiewicz 57c9ae8590 Initial implementation of DispatchQueue scheduler 2019-09-15 16:07:52 +03:00
Eric Patey d57c878651 FlatMap (#45)
Implement FlatMap
2019-09-13 10:57:17 -04:00
Eric Patey 7fa91778c2 Fix failing tests caused by Apple changes in GM Seed. (#56)
Update tests and implementation to match Apple changes in gm seed.
2019-09-13 10:50:38 -04:00
Sergej Jaskiewicz d15e604764 Remove mention of XCTestManifests from the Dangerfile 2019-09-13 14:45:28 +03:00
Evgeniy 07c7a98d72 @propertyWrapper Published (#52) 2019-09-07 18:10:53 +03:00
Sergej Jaskiewicz 01ef05be1f Pass --enable-index-store flag when testing in release mode 2019-09-07 16:32:34 +03:00
Sergej Jaskiewicz beee9d0d51 Remove allTests properties in test classes 2019-09-07 16:32:34 +03:00
Sergej Jaskiewicz aacd1a326c Remove LinuxMain.swift 2019-09-07 16:32:34 +03:00
Sergej Jaskiewicz 5528adcc67 Enable test discovery on Linux 2019-09-07 16:32:34 +03:00
Sergej Jaskiewicz 1b810d0536 Update Swift version on Linux 2019-09-07 16:32:34 +03:00
Bogdan Vlad 8b25238154 ReplaceError implementation (#50) 2019-09-01 22:05:44 +03:00
Sergej Jaskiewicz 9b9915bde7 Fix Optional.Publisher.collect() operator specialization
Update for Xcode 11.0 beta 6
2019-08-26 03:51:12 +03:00
Eric Patey 27f01e5f21 Implement IgnoreOutput (#44) 2019-08-20 17:32:33 +03:00
Sergej Jaskiewicz 739eb47409 Update for Xcode 11 beta 6
Yes, it's the only change.
2019-08-20 10:54:17 +03:00
Sergej Jaskiewicz 14d5a90e89 Add Slack badge 2019-08-05 19:57:58 +03:00
Sergej Jaskiewicz 0e869bc861 Implement CompactMap (#32) 2019-08-02 18:59:53 +03:00
Joe Spadafora 2f38069166 First where (#29) 2019-08-02 18:55:51 +03:00
Sergej Jaskiewicz 97d07d0a14 Better linting for inheritance clauses and dictionary literals 2019-08-02 14:18:17 +03:00
Joe Spadafora d3888a3808 Implement Filter/TryFilter (#22)
* Adds filter and try filter implementations

* Implement Filter

* Remove @testable declaration

* Fix linting

* Updates tests and creates testing helper

* Fix allTests to include all tests

* Renames TestHelper to OperatorTestHelper and adds documentation

* Adds more test coverage

* Updates to use subclasses for filter / tryfilter

* Adds subscription test

* Fix subscriber demand to be lazy

* Fix CustomPublisherBase changes from master

* Fix iOS availability on test helper

* Updates availability for test functions

* Simplify Filter implementation, add more tests

* Ensure test suite consistency on Darwin and Linux

* Add missing tests to XCTestManifests.swift
2019-08-02 00:20:35 +03:00
Franz Busch d2b8709afb Store newly send value in internal variable inside CurrentValueObject (#39) 2019-08-01 23:34:27 +03:00
Sergej Jaskiewicz a28177e9c5 Cache homebrew artifacts 2019-08-01 15:49:00 +03:00
Sergej Jaskiewicz cef19fce4b Use Danger CI 2019-08-01 15:49:00 +03:00
Sergej Jaskiewicz 7f6bba62de Add .hound.yml 2019-08-01 11:52:46 +03:00
107 changed files with 7671 additions and 1954 deletions
+1
View File
@@ -2,6 +2,7 @@
/.build
/Packages
/*.xcodeproj
/.swiftpm
# Created by https://www.gitignore.io/api/Xcode
# Edit at https://www.gitignore.io/?templates=Xcode
+32
View File
@@ -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
View File
@@ -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
+12
View File
@@ -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")
}
+41
View File
@@ -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
View File
@@ -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
)
+1
View File
@@ -3,6 +3,7 @@
[![codecov](https://codecov.io/gh/broadwaylamb/OpenCombine/branch/master/graph/badge.svg)](https://codecov.io/gh/broadwaylamb/OpenCombine)
![Language](https://img.shields.io/badge/Swift-5.0-orange.svg)
![Platform](https://img.shields.io/badge/platform-Linux%20%7C%20macOS%20%7C%20iOS%20%7C%20watchOS%20%7C%20tvOS-lightgrey.svg)
[<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 -592
View File
@@ -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 elements 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 doesnt 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 doesnt 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 doesnt 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 */
+14 -9
View File
@@ -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 dont 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)
}
+3 -52
View File
@@ -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
}
}
+6 -20
View File
@@ -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))"
}
}
+10 -4
View File
@@ -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)
+146
View File
@@ -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)
}
}
}
+3 -6
View File
@@ -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
}
}
-53
View File
@@ -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()
}
}
+4 -4
View File
@@ -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()
}
+97
View File
@@ -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)
+2 -2
View File
@@ -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 {
+2 -2
View File
@@ -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))
+34 -24
View File
@@ -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 doesnt 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 doesnt 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
/// doesnt 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 elements 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 }
}
}
+1 -1
View File
@@ -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 instances time by the given interval.
///
/// - Parameter n: A time interval to advance.
/// - Returns: A dispatch queue time advanced by the given
/// interval from this instances 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
-15
View File
@@ -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 -19
View File
@@ -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