47 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
76 changed files with 3655 additions and 1167 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
+2 -2
View File
@@ -78,14 +78,14 @@ attributes:
custom_rules:
no_foundation_dependency:
included: Sources/OpenCombine
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
included: Sources/OpenCombine/
name: "No Dispatch Dependency"
regex: "^import.*Dispatch.*$"
message: "We don't want to depend on Dispatch"
@@ -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>
+7
View File
@@ -58,6 +58,13 @@ script:
make test-debug;
fi
fi
- if [[ $OPENCOMBINE_TEST == "YES" ]]; then
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;
+4 -1
View File
@@ -7,7 +7,10 @@ release:
swift build -c release
test-debug:
swift test -c debug --sanitize thread $(SWIFT_TEST_FLAGS)
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)
+9 -3
View File
@@ -6,14 +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 -150
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.
@@ -536,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.
@@ -743,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.
@@ -1782,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.
@@ -3037,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.
@@ -3156,17 +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
}
/// 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 -53
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,53 +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)
downstreamSubject = nil
}
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))"
}
}
@@ -9,7 +9,7 @@
/// 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] = []
+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)
}
}
}
-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()
}
@@ -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 }
}
}
@@ -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
}
}
}
@@ -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)
@@ -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
)
@@ -93,9 +93,9 @@ 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,
Upstream.Output == SubscriberType.Input
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)
@@ -130,6 +130,8 @@ extension Publishers {
}
}
extension Publishers.First: Equatable where Upstream: Equatable {}
private class _FirstWhere<Upstream: Publisher, Downstream: Subscriber>
: OperatorSubscription<Downstream>,
Subscription
@@ -15,13 +15,14 @@ extension Publisher {
/// - 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.
/// 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>(maxPublishers: Subscribers.Demand = .unlimited,
_ transform: @escaping (Self.Output) -> Child)
-> Publishers.FlatMap<Child, Self>
where Result == Child.Output, Child: Publisher, Self.Failure == Child.Failure {
/// 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)
@@ -29,9 +30,10 @@ extension Publisher {
}
extension Publishers {
public struct FlatMap<Child, Upstream>: Publisher
where Child: Publisher, Upstream: Publisher, Child.Failure == Upstream.Failure {
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
@@ -60,23 +62,24 @@ extension Publishers {
/// - Parameters:
/// - subscriber: The subscriber to attach to this `Publisher`.
/// once attached it can begin to receive values.
public func receive<Downstream>(subscriber: Downstream)
where Downstream: Subscriber,
Child.Output == Downstream.Input,
Upstream.Failure == Downstream.Failure {
let inner = Inner(downstream: subscriber,
maxPublishers: maxPublishers,
transform: transform)
upstream.subscribe(inner)
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 {
Cancellable
where Downstream.Input == Child.Output, Downstream.Failure == Upstream.Failure
{
typealias Input = Upstream.Output
typealias Failure = Upstream.Failure
@@ -85,11 +88,13 @@ extension Publishers.FlatMap {
// 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?)
pausedChild: ChildSubscriber?
)
private let lock = Lock(recursive: false)
private let lock = unfairLock()
private let maxPublishers: Subscribers.Demand
private let transform: (Upstream.Output) -> Child
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
@@ -114,12 +119,14 @@ extension Publishers.FlatMap {
}
final func cancel() {
let (upstreamToCancel, childrenToCancel) = lock.do { ()
-> (Subscription?, Set<ChildSubscriber>) in
let upstreamToCancel = upstreamSubscription
upstreamSubscription = nil
return (upstreamToCancel, lockedDeactivateAndReturnChildToCancel())
}
let (upstreamToCancel, childrenToCancel) = lock
.do { () -> (Subscription?, Set<ChildSubscriber>) in
let upstreamToCancel = upstreamSubscription
upstreamSubscription = nil
return (upstreamToCancel, lockedDeactivateAndReturnChildrenToCancel())
}
upstreamToCancel?.cancel()
cancelChildren(childrenToCancel)
}
@@ -128,12 +135,13 @@ extension Publishers.FlatMap {
// Private implementation
extension Publishers.FlatMap.Inner {
private func deactivate() {
cancelChildren(lock.do(lockedDeactivateAndReturnChildToCancel))
cancelChildren(lock.do(lockedDeactivateAndReturnChildrenToCancel))
}
// Must be called with lock held.
private func lockedDeactivateAndReturnChildToCancel() -> Set<ChildSubscriber> {
private func lockedDeactivateAndReturnChildrenToCancel() -> Set<ChildSubscriber> {
downstream = nil
downstreamDemand = .none
let result = childSubscribers
@@ -212,7 +220,8 @@ extension Publishers.FlatMap.Inner {
}
let demandResult = surplusAvailable || demandForChild() == .unlimited
? Subscribers.Demand.none : Subscribers.Demand.max(1)
? Subscribers.Demand.none
: .max(1)
if processTheQueue {
processQueue()
@@ -222,7 +231,7 @@ extension Publishers.FlatMap.Inner {
}
private func demandForChild() -> Subscribers.Demand {
return self.downstreamDemand == .unlimited ? .unlimited : .max(1)
return downstreamDemand == .unlimited ? .unlimited : .max(1)
}
private enum QueueWorkStatus {
@@ -284,7 +293,7 @@ extension Publishers.FlatMap.Inner {
// This `Subscriber` implementation is for `FlatMap`'s upstream subscription
extension Publishers.FlatMap.Inner: Subscriber {
// FlatMap received the upstream subscription
fileprivate func receive(subscription: Subscription) {
upstreamSubscription = subscription
downstream?.receive(subscription: self)
@@ -304,7 +313,6 @@ extension Publishers.FlatMap.Inner: Subscriber {
return .none
}
// Upstream subscription completed
fileprivate func receive(completion: Subscribers.Completion<Failure>) {
switch completion {
case .finished:
@@ -320,8 +328,7 @@ extension Publishers.FlatMap.Inner: Subscriber {
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)
let becameUnlimited = demand == .unlimited && downstreamDemand != .unlimited
downstreamDemand = demand
defer { queueIsBeingProcessed = true }
return (!queueIsBeingProcessed, becameUnlimited)
@@ -377,14 +384,15 @@ extension Publishers.FlatMap.Inner {
}
extension Publishers.FlatMap.Inner.ChildSubscriber: Cancellable {
internal func cancel() {
fileprivate func cancel() {
_upstreamSubscription?.cancel()
_upstreamSubscription = nil
}
}
extension Publishers.FlatMap.Inner.ChildSubscriber: Subscriber {
internal func receive(subscription: Subscription) {
fileprivate func receive(subscription: Subscription) {
if _upstreamSubscription == nil {
_upstreamSubscription = subscription
subscription.request(_parent.demandForChild())
@@ -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()
}
}
}
@@ -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: "")
}
}
@@ -5,6 +5,19 @@
// 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 {
@@ -36,97 +49,131 @@ 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.Output == SubscriberType.Input,
SubscriberType.Failure == Failure
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Upstream.Output == Downstream.Input, Downstream.Failure == Failure
{
let replaceErrorSubscriber = _ReplaceError<Upstream, SubscriberType>(
downstream: subscriber,
output: output
)
upstream.subscribe(replaceErrorSubscriber)
upstream.subscribe(Inner(downstream: subscriber, output: output))
}
}
}
extension Publisher {
/// Replaces any errors in the stream with the provided element.
///
/// If the upstream publisher fails with an error, this publisher emits the provided
/// element, then finishes normally.
/// - Parameter output: An element to emit when the upstream publisher fails.
/// - Returns: A publisher that replaces an error from the upstream publisher with
/// the provided output element.
public func replaceError(with output: Output) -> Publishers.ReplaceError<Self> {
return Publishers.ReplaceError(upstream: self, output: output)
}
}
extension Publishers.ReplaceError: Equatable
where Upstream: Equatable, Upstream.Output: Equatable
{}
private final class _ReplaceError<Upstream: Publisher, Downstream: Subscriber>
: OperatorSubscription<Downstream>,
Subscriber,
CustomStringConvertible,
Subscription
where Upstream.Output == Downstream.Input
{
typealias Input = Upstream.Output
typealias Failure = Upstream.Failure
extension Publishers.ReplaceError {
var downstreamDemandCounter: Subscribers.Demand = .none
var hasFailed: Bool = false
var description: String { return "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.
private let output: Downstream.Input
typealias Input = Upstream.Output
typealias Failure = Upstream.Failure
init(downstream: Downstream,
output: Downstream.Input) {
self.output = output
super.init(downstream: downstream)
}
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()
func request(_ demand: Subscribers.Demand) {
if hasFailed {
_ = downstream.receive(output)
downstream?.receive(completion: .finished)
} else {
downstreamDemandCounter += demand
upstreamSubscription?.request(demand)
fileprivate init(downstream: Downstream, output: Upstream.Output) {
self.downstream = downstream
self.output = output
}
}
func receive(subscription: Subscription) {
upstreamSubscription = subscription
downstream.receive(subscription: self)
}
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 {
downstreamDemandCounter -= 1
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
}
let demand = downstream.receive(input)
downstreamDemandCounter += demand
return demand
}
func receive(completion: Subscribers.Completion<Upstream.Failure>) {
switch completion {
case .finished:
downstream.receive(completion: .finished)
case .failure:
hasFailed = true
// If there was no demand from downstream,
// ReplaceError does not forward the value that
// replaces the error until it is requested.
if downstreamDemandCounter > 0 {
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)
downstream.receive(completion: .finished)
}
}
}
override func cancel() {
upstreamSubscription?.cancel()
upstreamSubscription = nil
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 }
}
}
@@ -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: "status", 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
@@ -26,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])
@@ -44,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())
}
}
}
@@ -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
}
@@ -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) }
}
}
@@ -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
)
}
@@ -263,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 {}
@@ -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)
}
}
@@ -28,7 +28,7 @@ import OpenCombine
/// I understand that timeouts like this are a smell. I'd be happy to entertain other ways
/// to deterministically test concurrency/race conditions.
func performConcurrentBlock(_ block: @escaping () -> Void) {
private func performConcurrentBlock(_ block: @escaping () -> Void) {
let sem = DispatchSemaphore(value: 0)
DispatchQueue.global(qos: .background).async {
block()
@@ -46,34 +46,6 @@ func performConcurrentBlock(_ block: @escaping () -> Void) {
@available(macOS 10.15, iOS 13.0, *)
final class FlatMapTests: XCTestCase {
static let allTests = [
("testSendsChildValues", testSendsChildValues),
("testChildSubscribeDeadlock", testChildSubscribeDeadlock),
("testCancelCancels", testCancelCancels),
("testUpstreamDemandWithMaxPublishers", testUpstreamDemandWithMaxPublishers),
("testUpstreamDemandWithNoMaxPublishers", testUpstreamDemandWithNoMaxPublishers),
("testChildDemandWhenUnlimited", testChildDemandWhenUnlimited),
("testChildDemandWhenLimited", testChildDemandWhenLimited),
("testDemandFromLimitedtoUnlimited", testDemandFromLimitedToUnlimited),
("testChildValueReceivedWhileSendingValue",
testChildValueReceivedWhileSendingValue),
("testCompletesProperlyWhenChildrenOutliveUpstream",
testCompletesProperlyWhenChildrenOutliveUpstream),
("testCompletesProperlyWhenUpstreamOutlivesChildren",
testCompletesProperlyWhenUpstreamOutlivesChildren),
("testDoesNotCompleteWithBufferedValues", testDoesNotCompleteWithBufferedValues),
("testFailsIfUpstreamFails", testFailsIfUpstreamFails),
("testFailsIfChildFails", testFailsIfChildFails),
("testFailsWithoutSendingBufferedValues", testFailsWithoutSendingBufferedValues),
("testAllSubscriptionsReleasedOnUpstreamFailure",
testAllSubscriptionsReleasedOnUpstreamFailure),
("testAllSubscriptionsReleasedOnChildFailure",
testAllSubscriptionsReleasedOnChildFailure),
("testSendsSubcriptionDownstreamBeforeDemandUpstream",
testSendsSubcriptionDownstreamBeforeDemandUpstream),
("testTestSuiteIncludesAllTests", testTestSuiteIncludesAllTests)
]
func testSendsChildValues() {
let upstreamPublisher = PassthroughSubject<
PassthroughSubject<Int, TestingError>,
@@ -609,17 +581,4 @@ final class FlatMapTests: XCTestCase {
XCTAssertEqual(receiveOrder, [sentSubscriptionDownstream,
sentDemandRequestUpstream])
}
// MARK: -
func testTestSuiteIncludesAllTests() {
// https://oleb.net/blog/2017/03/keeping-xctest-in-sync/
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
let thisClass = type(of: self)
let allTestsCount = thisClass.allTests.count
let darwinCount = thisClass.defaultTestSuite.testCaseCount
XCTAssertEqual(allTestsCount,
darwinCount,
"\(darwinCount - allTestsCount) tests are missing from allTests")
#endif
}
}
@@ -33,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() {
@@ -67,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()
}
}
}
@@ -179,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
@@ -52,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
}
@@ -64,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() {
@@ -218,7 +226,7 @@ final class MapTests: XCTestCase {
}
func testTryMapCancel() throws {
// Given
let subscription = CustomSubscription()
let publisher = CustomPublisher(subscription: subscription)
let map = publisher.tryMap { $0 * 2 }
@@ -227,16 +235,17 @@ final class MapTests: XCTestCase {
$0.request(.unlimited)
downstreamSubscription = $0
})
// When
map.subscribe(tracking)
try XCTUnwrap(downstreamSubscription).cancel()
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)
@@ -258,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
@@ -17,100 +17,19 @@ import OpenCombine
final class MulticastTests: XCTestCase {
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() {
@@ -231,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)
@@ -277,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)
}
}
@@ -23,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")])
@@ -38,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),
@@ -70,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 {
@@ -18,7 +18,7 @@ final class PrintTests: XCTestCase {
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") }
@@ -68,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") }
@@ -155,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)
@@ -208,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) }
}
}
@@ -81,6 +81,11 @@ final class ReplaceErrorTests: XCTestCase {
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 {
@@ -154,8 +159,7 @@ final class ReplaceErrorTests: XCTestCase {
helper.downstreamSubscription?.request(.unlimited)
try XCTUnwrap(helper.downstreamSubscription).cancel()
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
.cancelled])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited), .cancelled])
}
func testErrorWhileDownstreamDemandIsZero() {
@@ -165,7 +169,7 @@ final class ReplaceErrorTests: XCTestCase {
createSut: { $0.replaceError(with: 42) })
// Send demanded value
_ = helper.publisher.send(9)
XCTAssertEqual(helper.publisher.send(9), .none)
XCTAssertEqual(helper.tracking.history, [.subscription("ReplaceError"),
.value(9)])
@@ -179,12 +183,71 @@ final class ReplaceErrorTests: XCTestCase {
.value(42),
.completion(.finished)])
}
}
private struct OtherError: Error {
let original: Error
func testCrashOnReceiveValueWithZeroPendingDemand() {
let publisher = CustomPublisher(subscription: CustomSubscription())
let replaceError = publisher.replaceError(with: 0)
let tracking = TrackingSubscriberBase<Int, Never>()
replaceError.subscribe(tracking)
init(_ original: Error) {
self.original = original
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)])
}
}
@@ -86,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) })
}
}
@@ -100,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 {
@@ -166,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
@@ -158,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)
}
}
@@ -61,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()
@@ -75,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() {
@@ -36,7 +36,7 @@ final class SinkTests: XCTestCase {
let subscription1 = CustomSubscription()
sink.receive(subscription: subscription1)
XCTAssertEqual(subscription1.lastRequested, .unlimited)
XCTAssertEqual(subscription1.history, [.requested(.unlimited)])
XCTAssertFalse(subscription1.cancelled)
let subscription2 = CustomSubscription()
@@ -50,6 +50,11 @@ final class SinkTests: XCTestCase {
subscription1.cancelled = false
sink.receive(completion: .finished)
XCTAssertFalse(subscription1.cancelled)
let subscription3 = CustomSubscription()
sink.receive(subscription: subscription3)
XCTAssertEqual(subscription3.history, [.cancelled])
XCTAssertTrue(subscription3.cancelled)
}
func testReceiveValue() {
@@ -0,0 +1,14 @@
CombineIdentifierTests:
testDefaultInitializedPerformance:
maxPercentRelativeStandardDeviation: 1e+1
measurement: 1.5804951544851065e-05
strategy: minimum
PassthroughSubjectTests:
testCancelSubscriptionPerformance:
maxPercentRelativeStandardDeviation: 1e+1
measurement: 6.59683800768107e-3
strategy: minimum
testSubscriptionPerformance:
maxPercentRelativeStandardDeviation: 1e+1
measurement: 1.56868119956926e-2
strategy: minimum
@@ -1,5 +1,5 @@
CombineIdentifierTests:
testDefaultInitializedPerformance:
maxPercentRelativeStandardDeviation: 1e+1
measurement: 1.06726900003764e-4
measurement: 1.6446406001023208e-05
strategy: minimum
@@ -1,5 +1,5 @@
CombineIdentifierTests:
testDefaultInitializedPerformance:
maxPercentRelativeStandardDeviation: 1e+1
measurement: 1.47168469999883e-4
measurement: 2.1368432000258509e-05
strategy: minimum