Compare commits
25 Commits
0.6.0
...
merge-publisher
| Author | SHA1 | Date | |
|---|---|---|---|
| af0ae86407 | |||
| f861335dc3 | |||
| 769c3c818f | |||
| 910d21da4c | |||
| 6e20956d6d | |||
| e453879d75 | |||
| 98f6b6b337 | |||
| 74b739d74e | |||
| bcba9a19d4 | |||
| 486e166462 | |||
| c6536cf8d3 | |||
| cf41c25cf7 | |||
| b4557fb311 | |||
| f8e6e66ab4 | |||
| 95b42abce3 | |||
| 899a04bb3f | |||
| 5f9a700689 | |||
| a300fd09d3 | |||
| 5973f86c6e | |||
| 1b5afdba26 | |||
| 51d5d1e71d | |||
| a8bc5cc046 | |||
| 86d6170dc9 | |||
| 171131d768 | |||
| d6b4fb4115 |
+22
-16
@@ -1,16 +1,17 @@
|
||||
version: 2
|
||||
jobs:
|
||||
"Execute tests on macOS 10.15.0 (Xcode 11.2.0, Swift 5.1.2)":
|
||||
"Execute tests on macOS 10.15.0 (Xcode 11.3.0, Swift 5.1.3)":
|
||||
macos:
|
||||
xcode: "11.2.0"
|
||||
xcode: "11.3.0"
|
||||
environment:
|
||||
SWIFT_VERSION: "5.1.2"
|
||||
SWIFT_VERSION: "5.1.3"
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Building and running tests in debug mode with coverage
|
||||
command: |
|
||||
make test-debug \
|
||||
SWIFT_BUILD_FLAGS="-Xswiftc -warnings-as-errors" \
|
||||
SWIFT_TEST_FLAGS="--enable-code-coverage --build-path .build-test-debug"
|
||||
xcrun llvm-cov show \
|
||||
-instr-profile=.build-test-debug/debug/codecov/default.profdata \
|
||||
@@ -20,15 +21,17 @@ jobs:
|
||||
name: Building and running tests in debug mode with TSan
|
||||
command: |
|
||||
make test-debug-sanitize-thread \
|
||||
SWIFT_BUILD_FLAGS="-Xswiftc -warnings-as-errors" \
|
||||
SWIFT_TEST_FLAGS="--build-path .build-test-debug-sanitize-thread"
|
||||
- run:
|
||||
name: Building and running tests in release mode
|
||||
command: |
|
||||
make test-release \
|
||||
SWIFT_BUILD_FLAGS="-Xswiftc -warnings-as-errors" \
|
||||
SWIFT_TEST_FLAGS="--build-path .build-test-release"
|
||||
- run:
|
||||
name: Generating Xcode project
|
||||
command: make generate-xcodeproj
|
||||
command: make generate-xcodeproj SWIFT_BUILD_FLAGS="-Xswiftc -warnings-as-errors"
|
||||
- run:
|
||||
name: Building for testing on macOS 10.15.0 with xcodebuild
|
||||
command: |
|
||||
@@ -60,35 +63,35 @@ jobs:
|
||||
command: |
|
||||
bash <(curl -s https://codecov.io/bash) -D DerivedData
|
||||
|
||||
"Execute compatibility tests on iOS 13.2.2 (Xcode 11.2.0, Swift 5.1.2)":
|
||||
"Execute compatibility tests on iOS 13.3 (Xcode 11.3.0, Swift 5.1.3)":
|
||||
macos:
|
||||
xcode: "11.2.0"
|
||||
xcode: "11.3.0"
|
||||
environment:
|
||||
SWIFT_VERSION: "5.1.2"
|
||||
SWIFT_VERSION: "5.1.3"
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Generating Xcode project
|
||||
command: make generate-compatibility-xcodeproj
|
||||
- run:
|
||||
name: Building for testing on iOS 13.2.2 with xcodebuild
|
||||
name: Building for testing on iOS 13.3 with xcodebuild
|
||||
command: |
|
||||
set -o pipefail \
|
||||
&& xcodebuild build-for-testing \
|
||||
-scheme OpenCombine-Package \
|
||||
-destination "platform=iOS Simulator,name=iPhone 11,OS=13.2.2" \
|
||||
-destination "platform=iOS Simulator,name=iPhone 11,OS=13.3" \
|
||||
-derivedDataPath DerivedData \
|
||||
| tee xcodebuild_build-for-testing.log \
|
||||
| xcpretty
|
||||
- store_artifacts:
|
||||
path: xcodebuild_build-for-testing.log
|
||||
- run:
|
||||
name: Testing against Combine on iOS 13.2.2 with xcodebuild
|
||||
name: Testing against Combine on iOS 13.3 with xcodebuild
|
||||
command: |
|
||||
set -o pipefail \
|
||||
&& xcodebuild test-without-building \
|
||||
-scheme OpenCombine-Package \
|
||||
-destination "platform=iOS Simulator,name=iPhone 11,OS=13.2.2" \
|
||||
-destination "platform=iOS Simulator,name=iPhone 11,OS=13.3" \
|
||||
-derivedDataPath DerivedData \
|
||||
| tee xcodebuild_test-without-building.log \
|
||||
| xcpretty --report junit -o build/reports/results.xml
|
||||
@@ -125,7 +128,7 @@ jobs:
|
||||
- run:
|
||||
name: Generating Xcode project
|
||||
command: |
|
||||
make generate-xcodeproj
|
||||
make generate-xcodeproj SWIFT_BUILD_FLAGS="-Xswiftc -warnings-as-errors"
|
||||
xcodebuild -scheme OpenCombine-Package -showdestinations
|
||||
- run:
|
||||
name: Building for testing on iOS 9.3 with xcodebuild
|
||||
@@ -182,6 +185,7 @@ jobs:
|
||||
> /dev/null 2>&1 \
|
||||
|| true
|
||||
make test-debug \
|
||||
SWIFT_BUILD_FLAGS="-Xswiftc -warnings-as-errors" \
|
||||
SWIFT_TEST_FLAGS="--enable-test-discovery \
|
||||
--enable-index-store \
|
||||
--enable-code-coverage \
|
||||
@@ -194,6 +198,7 @@ jobs:
|
||||
name: Building and running tests in debug mode with TSan
|
||||
command: | # We need to run the test command twice because of https://bugs.swift.org/browse/SR-10783
|
||||
make test-debug-sanitize-thread \
|
||||
SWIFT_BUILD_FLAGS="-Xswiftc -warnings-as-errors" \
|
||||
SWIFT_TEST_FLAGS="--enable-test-discovery \
|
||||
--enable-index-store \
|
||||
--build-path .build-test-debug-sanitize-thread" \
|
||||
@@ -207,6 +212,7 @@ jobs:
|
||||
name: Building and running tests in release mode
|
||||
command: |
|
||||
make test-release \
|
||||
SWIFT_BUILD_FLAGS="-Xswiftc -warnings-as-errors" \
|
||||
SWIFT_TEST_FLAGS="--enable-test-discovery \
|
||||
--enable-index-store \
|
||||
--build-path .build-test-release"
|
||||
@@ -217,7 +223,7 @@ jobs:
|
||||
|
||||
"Run SwiftLint and Danger":
|
||||
macos:
|
||||
xcode: "11.2.0"
|
||||
xcode: "11.3.0"
|
||||
environment:
|
||||
HOMEBREW_NO_AUTO_UPDATE: "1"
|
||||
steps:
|
||||
@@ -236,7 +242,7 @@ jobs:
|
||||
|
||||
"Run Pod spec lint":
|
||||
macos:
|
||||
xcode: "11.2.0"
|
||||
xcode: "11.3.0"
|
||||
environment:
|
||||
HOMEBREW_NO_AUTO_UPDATE: "1"
|
||||
steps:
|
||||
@@ -250,10 +256,10 @@ workflows:
|
||||
version: 2
|
||||
"OpenCombine: execute tests on macOS":
|
||||
jobs:
|
||||
- "Execute tests on macOS 10.15.0 (Xcode 11.2.0, Swift 5.1.2)"
|
||||
- "Execute tests on macOS 10.15.0 (Xcode 11.3.0, Swift 5.1.3)"
|
||||
"OpenCombine: execute compatibility tests":
|
||||
jobs:
|
||||
- "Execute compatibility tests on iOS 13.2.2 (Xcode 11.2.0, Swift 5.1.2)"
|
||||
- "Execute compatibility tests on iOS 13.3 (Xcode 11.3.0, Swift 5.1.3)"
|
||||
"OpenCombine: execute tests on iOS":
|
||||
jobs:
|
||||
- "Execute tests on iOS 9.3 (Xcode 10.2.1, Swift 5.0.1)"
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
Pod::Spec.new do |spec|
|
||||
spec.name = "COpenCombineHelpers"
|
||||
spec.version = "0.5.0"
|
||||
spec.summary = "C++ Helpers for OpenCombine"
|
||||
|
||||
spec.description = <<-DESC
|
||||
C++ helpers necessary for the implementation of OpenCombine
|
||||
DESC
|
||||
|
||||
spec.homepage = "https://github.com/broadwaylamb/OpenCombine/"
|
||||
spec.license = "MIT"
|
||||
|
||||
spec.authors = { "Sergej Jaskiewicz" => "jaskiewiczs@icloud.com" }
|
||||
spec.source = { :git => "https://github.com/broadwaylamb/OpenCombine.git", :tag => "#{spec.version}" }
|
||||
|
||||
spec.osx.deployment_target = "10.10"
|
||||
spec.ios.deployment_target = "8.0"
|
||||
spec.watchos.deployment_target = "2.0"
|
||||
spec.tvos.deployment_target = "9.0"
|
||||
|
||||
spec.header_mappings_dir = "Sources/COpenCombineHelpers/include"
|
||||
spec.source_files = "Sources/COpenCombineHelpers/**/*.{cpp,h}"
|
||||
spec.libraries = "c++"
|
||||
|
||||
spec.pod_target_xcconfig = {
|
||||
"DEFINES_MODULE" => "YES"
|
||||
}
|
||||
end
|
||||
+1
-1
@@ -18,7 +18,7 @@ GEM
|
||||
unf (>= 0.0.5, < 1.0.0)
|
||||
dotenv (2.7.5)
|
||||
emoji_regex (1.0.1)
|
||||
excon (0.68.0)
|
||||
excon (0.71.0)
|
||||
faraday (0.17.0)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
faraday-cookie_jar (0.0.6)
|
||||
|
||||
+5
-3
@@ -1,6 +1,6 @@
|
||||
Pod::Spec.new do |spec|
|
||||
spec.name = "OpenCombine"
|
||||
spec.version = "0.6.0"
|
||||
spec.version = "0.7.0"
|
||||
spec.summary = "Open source implementation of Apple's Combine framework for processing values over time."
|
||||
|
||||
spec.description = <<-DESC
|
||||
@@ -20,6 +20,8 @@ Pod::Spec.new do |spec|
|
||||
spec.watchos.deployment_target = "2.0"
|
||||
spec.tvos.deployment_target = "9.0"
|
||||
|
||||
spec.source_files = "Sources/OpenCombine/**/*.swift"
|
||||
spec.dependency "COpenCombineHelpers"
|
||||
spec.source_files = "Sources/COpenCombineHelpers/**/*.{h,cpp}", "Sources/OpenCombine/**/*.swift"
|
||||
spec.public_header_files = "Sources/COpenCombineHelpers/include/*.h"
|
||||
|
||||
spec.libraries = "c++"
|
||||
end
|
||||
@@ -1,10 +1,10 @@
|
||||
Pod::Spec.new do |spec|
|
||||
spec.name = "OpenCombineDispatch"
|
||||
spec.version = "0.6.0"
|
||||
spec.version = "0.7.0"
|
||||
spec.summary = "OpenCombine + Dispatch interoperability"
|
||||
|
||||
spec.description = <<-DESC
|
||||
Extends `DispatchQueue` with new methods and nested types.
|
||||
Extends `DispatchQueue` with conformance to the `Scheduler` protocol
|
||||
DESC
|
||||
|
||||
spec.homepage = "https://github.com/broadwaylamb/OpenCombine/"
|
||||
@@ -21,5 +21,5 @@ Pod::Spec.new do |spec|
|
||||
spec.tvos.deployment_target = "9.0"
|
||||
|
||||
spec.source_files = "Sources/OpenCombineDispatch/**/*.swift"
|
||||
spec.dependency "OpenCombine"
|
||||
spec.dependency "OpenCombine", '>= 0.6'
|
||||
end
|
||||
@@ -0,0 +1,25 @@
|
||||
Pod::Spec.new do |spec|
|
||||
spec.name = "OpenCombineFoundation"
|
||||
spec.version = "0.7.0"
|
||||
spec.summary = "OpenCombine + OpenCombineFoundation interoperability"
|
||||
|
||||
spec.description = <<-DESC
|
||||
Adds publishers to Foundation types like NotificationCenter, URLSession etc.
|
||||
DESC
|
||||
|
||||
spec.homepage = "https://github.com/broadwaylamb/OpenCombine/"
|
||||
spec.license = "MIT"
|
||||
|
||||
spec.authors = { "Sergej Jaskiewicz" => "jaskiewiczs@icloud.com" }
|
||||
spec.source = { :git => "https://github.com/broadwaylamb/OpenCombine.git", :tag => "#{spec.version}" }
|
||||
|
||||
spec.swift_version = "5.0"
|
||||
|
||||
spec.osx.deployment_target = "10.10"
|
||||
spec.ios.deployment_target = "8.0"
|
||||
spec.watchos.deployment_target = "2.0"
|
||||
spec.tvos.deployment_target = "9.0"
|
||||
|
||||
spec.source_files = "Sources/OpenCombineFoundation/**/*.swift"
|
||||
spec.dependency "OpenCombine", '~> 0.6'
|
||||
end
|
||||
+5
-1
@@ -7,14 +7,18 @@ let package = Package(
|
||||
products: [
|
||||
.library(name: "OpenCombine", targets: ["OpenCombine"]),
|
||||
.library(name: "OpenCombineDispatch", targets: ["OpenCombineDispatch"]),
|
||||
.library(name: "OpenCombineFoundation", targets: ["OpenCombineFoundation"]),
|
||||
],
|
||||
targets: [
|
||||
.target(name: "COpenCombineHelpers"),
|
||||
.target(name: "OpenCombine", dependencies: ["COpenCombineHelpers"]),
|
||||
.target(name: "OpenCombineDispatch", dependencies: ["OpenCombine"]),
|
||||
.target(name: "OpenCombineFoundation", dependencies: ["OpenCombine",
|
||||
"COpenCombineHelpers"]),
|
||||
.testTarget(name: "OpenCombineTests",
|
||||
dependencies: ["OpenCombine",
|
||||
"OpenCombineDispatch"],
|
||||
"OpenCombineDispatch",
|
||||
"OpenCombineFoundation"],
|
||||
swiftSettings: [.unsafeFlags(["-enable-testing"])])
|
||||
],
|
||||
cxxLanguageStandard: .cxx1z
|
||||
|
||||
@@ -23,7 +23,7 @@ To add `OpenCombine` to your [SPM](https://swift.org/package-manager/) package,
|
||||
|
||||
```swift
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/broadwaylamb/OpenCombine.git", from: "0.6.0")
|
||||
.package(url: "https://github.com/broadwaylamb/OpenCombine.git", from: "0.7.0")
|
||||
],
|
||||
targets: [
|
||||
.target(name: "MyAwesomePackage", dependencies: ["OpenCombine", "OpenCombineDispatch"])
|
||||
@@ -44,8 +44,8 @@ To do so, open Xcode, use **File** → **Swift Packages** → **Add Package Depe
|
||||
To add `OpenCombine` to a project using [CocoaPods](https://cocoapods.org/), add `OpenCombine` and `OpenCombineDispatch` to the list of target dependencies in your `Podfile`.
|
||||
|
||||
```ruby
|
||||
pod 'OpenCombine', '~> 0.6.0'
|
||||
pod 'OpenCombineDispatch', '~> 0.6.0'
|
||||
pod 'OpenCombine', '~> 0.7'
|
||||
pod 'OpenCombineDispatch', '~> 0.7'
|
||||
```
|
||||
|
||||
### Contributing
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,163 @@
|
||||
// From /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/lib/swift/Foundation.swiftmodule/x86_64.swiftinterface
|
||||
// swift-interface-format-version: 1.0
|
||||
// swift-compiler-version: Apple Swift version 5.1.1 (swiftlang-1100.8.275.1 clang-1100.0.32.1)
|
||||
// swift-module-flags: -target x86_64-apple-macosx10.15 -enable-objc-interop -autolink-force-load -enable-library-evolution -module-link-name swiftFoundation -swift-version 5 -O -enforce-exclusivity=unchecked -module-name Foundation
|
||||
|
||||
public typealias Published = Combine.Published
|
||||
|
||||
public typealias ObservableObject = Combine.ObservableObject
|
||||
|
||||
public protocol _KeyValueCodingAndObservingPublishing {
|
||||
}
|
||||
|
||||
extension NSObject : Foundation._KeyValueCodingAndObservingPublishing {
|
||||
}
|
||||
|
||||
extension _KeyValueCodingAndObservingPublishing where Self : ObjectiveC.NSObject {
|
||||
public func publisher<Value>(for keyPath: Swift.KeyPath<Self, Value>, options: Foundation.NSKeyValueObservingOptions = [.initial, .new]) -> ObjectiveC.NSObject.KeyValueObservingPublisher<Self, Value>
|
||||
}
|
||||
|
||||
extension NSObject.KeyValueObservingPublisher {
|
||||
public func didChange() -> Combine.Publishers.Map<ObjectiveC.NSObject.KeyValueObservingPublisher<Subject, Value>, Swift.Void>
|
||||
}
|
||||
|
||||
extension NSObject {
|
||||
public struct KeyValueObservingPublisher<Subject, Value> : Swift.Equatable where Subject : ObjectiveC.NSObject {
|
||||
public let object: Subject
|
||||
public let keyPath: Swift.KeyPath<Subject, Value>
|
||||
public let options: Foundation.NSKeyValueObservingOptions
|
||||
public init(object: Subject, keyPath: Swift.KeyPath<Subject, Value>, options: Foundation.NSKeyValueObservingOptions)
|
||||
public static func == (lhs: ObjectiveC.NSObject.KeyValueObservingPublisher<Subject, Value>, rhs: ObjectiveC.NSObject.KeyValueObservingPublisher<Subject, Value>) -> Swift.Bool
|
||||
}
|
||||
}
|
||||
|
||||
extension NSObject.KeyValueObservingPublisher : Combine.Publisher {
|
||||
public typealias Output = Value
|
||||
public typealias Failure = Swift.Never
|
||||
public func receive<S>(subscriber: S) where Value == S.Input, S : Combine.Subscriber, S.Failure == ObjectiveC.NSObject.KeyValueObservingPublisher<Subject, Value>.Failure
|
||||
}
|
||||
|
||||
extension Timer {
|
||||
public static func publish(every interval: Foundation.TimeInterval, tolerance: Foundation.TimeInterval? = nil, on runLoop: Foundation.RunLoop, in mode: Foundation.RunLoop.Mode, options: Foundation.RunLoop.SchedulerOptions? = nil) -> Foundation.Timer.TimerPublisher
|
||||
final public class TimerPublisher : Combine.ConnectablePublisher {
|
||||
public typealias Output = Foundation.Date
|
||||
public typealias Failure = Swift.Never
|
||||
final public let interval: Foundation.TimeInterval
|
||||
final public let tolerance: Foundation.TimeInterval?
|
||||
final public let runLoop: Foundation.RunLoop
|
||||
final public let mode: Foundation.RunLoop.Mode
|
||||
final public let options: Foundation.RunLoop.SchedulerOptions?
|
||||
public init(interval: Foundation.TimeInterval, tolerance: Foundation.TimeInterval? = nil, runLoop: Foundation.RunLoop, mode: Foundation.RunLoop.Mode, options: Foundation.RunLoop.SchedulerOptions? = nil)
|
||||
final public func receive<S>(subscriber: S) where S : Combine.Subscriber, S.Failure == Foundation.Timer.TimerPublisher.Failure, S.Input == Foundation.Timer.TimerPublisher.Output
|
||||
final public func connect() -> Combine.Cancellable
|
||||
@objc deinit
|
||||
}
|
||||
}
|
||||
|
||||
extension OperationQueue : Combine.Scheduler {
|
||||
public struct SchedulerTimeType : Swift.Strideable, Swift.Codable, Swift.Hashable {
|
||||
public var date: Foundation.Date
|
||||
public init(_ date: Foundation.Date)
|
||||
public func distance(to other: Foundation.OperationQueue.SchedulerTimeType) -> Foundation.OperationQueue.SchedulerTimeType.Stride
|
||||
public func advanced(by n: Foundation.OperationQueue.SchedulerTimeType.Stride) -> Foundation.OperationQueue.SchedulerTimeType
|
||||
public struct Stride : Swift.ExpressibleByFloatLiteral, Swift.Comparable, Swift.SignedNumeric, Swift.Codable, Combine.SchedulerTimeIntervalConvertible {
|
||||
public typealias FloatLiteralType = Foundation.TimeInterval
|
||||
public typealias IntegerLiteralType = Foundation.TimeInterval
|
||||
public typealias Magnitude = Foundation.TimeInterval
|
||||
public var magnitude: Foundation.TimeInterval
|
||||
public var timeInterval: Foundation.TimeInterval {
|
||||
get
|
||||
}
|
||||
public init(integerLiteral value: Foundation.TimeInterval)
|
||||
public init(floatLiteral value: Foundation.TimeInterval)
|
||||
public init(_ timeInterval: Foundation.TimeInterval)
|
||||
public init?<T>(exactly source: T) where T : Swift.BinaryInteger
|
||||
public static func < (lhs: Foundation.OperationQueue.SchedulerTimeType.Stride, rhs: Foundation.OperationQueue.SchedulerTimeType.Stride) -> Swift.Bool
|
||||
public static func * (lhs: Foundation.OperationQueue.SchedulerTimeType.Stride, rhs: Foundation.OperationQueue.SchedulerTimeType.Stride) -> Foundation.OperationQueue.SchedulerTimeType.Stride
|
||||
public static func + (lhs: Foundation.OperationQueue.SchedulerTimeType.Stride, rhs: Foundation.OperationQueue.SchedulerTimeType.Stride) -> Foundation.OperationQueue.SchedulerTimeType.Stride
|
||||
public static func - (lhs: Foundation.OperationQueue.SchedulerTimeType.Stride, rhs: Foundation.OperationQueue.SchedulerTimeType.Stride) -> Foundation.OperationQueue.SchedulerTimeType.Stride
|
||||
public static func *= (lhs: inout Foundation.OperationQueue.SchedulerTimeType.Stride, rhs: Foundation.OperationQueue.SchedulerTimeType.Stride)
|
||||
public static func += (lhs: inout Foundation.OperationQueue.SchedulerTimeType.Stride, rhs: Foundation.OperationQueue.SchedulerTimeType.Stride)
|
||||
public static func -= (lhs: inout Foundation.OperationQueue.SchedulerTimeType.Stride, rhs: Foundation.OperationQueue.SchedulerTimeType.Stride)
|
||||
public static func seconds(_ s: Swift.Int) -> Foundation.OperationQueue.SchedulerTimeType.Stride
|
||||
public static func seconds(_ s: Swift.Double) -> Foundation.OperationQueue.SchedulerTimeType.Stride
|
||||
public static func milliseconds(_ ms: Swift.Int) -> Foundation.OperationQueue.SchedulerTimeType.Stride
|
||||
public static func microseconds(_ us: Swift.Int) -> Foundation.OperationQueue.SchedulerTimeType.Stride
|
||||
public static func nanoseconds(_ ns: Swift.Int) -> Foundation.OperationQueue.SchedulerTimeType.Stride
|
||||
public init(from decoder: Swift.Decoder) throws
|
||||
public func encode(to encoder: Swift.Encoder) throws
|
||||
public static func == (a: Foundation.OperationQueue.SchedulerTimeType.Stride, b: Foundation.OperationQueue.SchedulerTimeType.Stride) -> Swift.Bool
|
||||
}
|
||||
public init(from decoder: Swift.Decoder) throws
|
||||
public func encode(to encoder: Swift.Encoder) throws
|
||||
public var hashValue: Swift.Int {
|
||||
get
|
||||
}
|
||||
public func hash(into hasher: inout Swift.Hasher)
|
||||
}
|
||||
public struct SchedulerOptions {
|
||||
}
|
||||
public func schedule(options: Foundation.OperationQueue.SchedulerOptions?, _ action: @escaping () -> Swift.Void)
|
||||
public func schedule(after date: Foundation.OperationQueue.SchedulerTimeType, tolerance: Foundation.OperationQueue.SchedulerTimeType.Stride, options: Foundation.OperationQueue.SchedulerOptions?, _ action: @escaping () -> Swift.Void)
|
||||
public func schedule(after date: Foundation.OperationQueue.SchedulerTimeType, interval: Foundation.OperationQueue.SchedulerTimeType.Stride, tolerance: Foundation.OperationQueue.SchedulerTimeType.Stride, options: Foundation.OperationQueue.SchedulerOptions?, _ action: @escaping () -> Swift.Void) -> Combine.Cancellable
|
||||
public var now: Foundation.OperationQueue.SchedulerTimeType {
|
||||
get
|
||||
}
|
||||
public var minimumTolerance: Foundation.OperationQueue.SchedulerTimeType.Stride {
|
||||
get
|
||||
}
|
||||
}
|
||||
|
||||
extension RunLoop : Combine.Scheduler {
|
||||
public struct SchedulerTimeType : Swift.Strideable, Swift.Codable, Swift.Hashable {
|
||||
public var date: Foundation.Date
|
||||
public init(_ date: Foundation.Date)
|
||||
public func distance(to other: Foundation.RunLoop.SchedulerTimeType) -> Foundation.RunLoop.SchedulerTimeType.Stride
|
||||
public func advanced(by n: Foundation.RunLoop.SchedulerTimeType.Stride) -> Foundation.RunLoop.SchedulerTimeType
|
||||
public struct Stride : Swift.ExpressibleByFloatLiteral, Swift.Comparable, Swift.SignedNumeric, Swift.Codable, Combine.SchedulerTimeIntervalConvertible {
|
||||
public typealias FloatLiteralType = Foundation.TimeInterval
|
||||
public typealias IntegerLiteralType = Foundation.TimeInterval
|
||||
public typealias Magnitude = Foundation.TimeInterval
|
||||
public var magnitude: Foundation.TimeInterval
|
||||
public var timeInterval: Foundation.TimeInterval {
|
||||
get
|
||||
}
|
||||
public init(integerLiteral value: Foundation.TimeInterval)
|
||||
public init(floatLiteral value: Foundation.TimeInterval)
|
||||
public init(_ timeInterval: Foundation.TimeInterval)
|
||||
public init?<T>(exactly source: T) where T : Swift.BinaryInteger
|
||||
public static func < (lhs: Foundation.RunLoop.SchedulerTimeType.Stride, rhs: Foundation.RunLoop.SchedulerTimeType.Stride) -> Swift.Bool
|
||||
public static func * (lhs: Foundation.RunLoop.SchedulerTimeType.Stride, rhs: Foundation.RunLoop.SchedulerTimeType.Stride) -> Foundation.RunLoop.SchedulerTimeType.Stride
|
||||
public static func + (lhs: Foundation.RunLoop.SchedulerTimeType.Stride, rhs: Foundation.RunLoop.SchedulerTimeType.Stride) -> Foundation.RunLoop.SchedulerTimeType.Stride
|
||||
public static func - (lhs: Foundation.RunLoop.SchedulerTimeType.Stride, rhs: Foundation.RunLoop.SchedulerTimeType.Stride) -> Foundation.RunLoop.SchedulerTimeType.Stride
|
||||
public static func *= (lhs: inout Foundation.RunLoop.SchedulerTimeType.Stride, rhs: Foundation.RunLoop.SchedulerTimeType.Stride)
|
||||
public static func += (lhs: inout Foundation.RunLoop.SchedulerTimeType.Stride, rhs: Foundation.RunLoop.SchedulerTimeType.Stride)
|
||||
public static func -= (lhs: inout Foundation.RunLoop.SchedulerTimeType.Stride, rhs: Foundation.RunLoop.SchedulerTimeType.Stride)
|
||||
public static func seconds(_ s: Swift.Int) -> Foundation.RunLoop.SchedulerTimeType.Stride
|
||||
public static func seconds(_ s: Swift.Double) -> Foundation.RunLoop.SchedulerTimeType.Stride
|
||||
public static func milliseconds(_ ms: Swift.Int) -> Foundation.RunLoop.SchedulerTimeType.Stride
|
||||
public static func microseconds(_ us: Swift.Int) -> Foundation.RunLoop.SchedulerTimeType.Stride
|
||||
public static func nanoseconds(_ ns: Swift.Int) -> Foundation.RunLoop.SchedulerTimeType.Stride
|
||||
public init(from decoder: Swift.Decoder) throws
|
||||
public func encode(to encoder: Swift.Encoder) throws
|
||||
public static func == (a: Foundation.RunLoop.SchedulerTimeType.Stride, b: Foundation.RunLoop.SchedulerTimeType.Stride) -> Swift.Bool
|
||||
}
|
||||
public init(from decoder: Swift.Decoder) throws
|
||||
public func encode(to encoder: Swift.Encoder) throws
|
||||
public var hashValue: Swift.Int {
|
||||
get
|
||||
}
|
||||
public func hash(into hasher: inout Swift.Hasher)
|
||||
}
|
||||
public struct SchedulerOptions {
|
||||
}
|
||||
public func schedule(options: Foundation.RunLoop.SchedulerOptions?, _ action: @escaping () -> Swift.Void)
|
||||
public func schedule(after date: Foundation.RunLoop.SchedulerTimeType, tolerance: Foundation.RunLoop.SchedulerTimeType.Stride, options: Foundation.RunLoop.SchedulerOptions?, _ action: @escaping () -> Swift.Void)
|
||||
public func schedule(after date: Foundation.RunLoop.SchedulerTimeType, interval: Foundation.RunLoop.SchedulerTimeType.Stride, tolerance: Foundation.RunLoop.SchedulerTimeType.Stride, options: Foundation.RunLoop.SchedulerOptions?, _ action: @escaping () -> Swift.Void) -> Combine.Cancellable
|
||||
public var now: Foundation.RunLoop.SchedulerTimeType {
|
||||
get
|
||||
}
|
||||
public var minimumTolerance: Foundation.RunLoop.SchedulerTimeType.Stride {
|
||||
get
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
#define COPENCOMBINEHELPERS_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <signal.h>
|
||||
|
||||
#if __has_attribute(swift_name)
|
||||
# define OPENCOMBINE_SWIFT_NAME(_name) __attribute__((swift_name(#_name)))
|
||||
@@ -16,6 +17,12 @@
|
||||
# define OPENCOMBINE_SWIFT_NAME(_name)
|
||||
#endif
|
||||
|
||||
#if __has_attribute(always_inline)
|
||||
# define OPENCOMBINE_ALWAYS_INLINE __attribute__((always_inline))
|
||||
#else
|
||||
# define OPENCOMBINE_ALWAYS_INLINE
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
@@ -23,50 +30,59 @@ extern "C" {
|
||||
#pragma mark - CombineIdentifier
|
||||
|
||||
uint64_t opencombine_next_combine_identifier(void)
|
||||
OPENCOMBINE_SWIFT_NAME(nextCombineIdentifier());
|
||||
OPENCOMBINE_SWIFT_NAME(__nextCombineIdentifier());
|
||||
|
||||
#pragma mark - OpenCombineUnfairLock
|
||||
|
||||
/// A wrapper around an opaque pointer for type safety in Swift.
|
||||
typedef struct OpenCombineUnfairLock {
|
||||
void* _Nonnull opaque;
|
||||
} OPENCOMBINE_SWIFT_NAME(UnfairLock) OpenCombineUnfairLock;
|
||||
} OPENCOMBINE_SWIFT_NAME(__UnfairLock) OpenCombineUnfairLock;
|
||||
|
||||
/// Allocates a lock object. The allocated object must be destroyed by calling
|
||||
/// the destroy() method.
|
||||
OpenCombineUnfairLock opencombine_unfair_lock_alloc(void)
|
||||
OPENCOMBINE_SWIFT_NAME(UnfairLock.allocate());
|
||||
OPENCOMBINE_SWIFT_NAME(__UnfairLock.allocate());
|
||||
|
||||
void opencombine_unfair_lock_lock(OpenCombineUnfairLock)
|
||||
OPENCOMBINE_SWIFT_NAME(UnfairLock.lock(self:));
|
||||
OPENCOMBINE_SWIFT_NAME(__UnfairLock.lock(self:));
|
||||
|
||||
void opencombine_unfair_lock_unlock(OpenCombineUnfairLock)
|
||||
OPENCOMBINE_SWIFT_NAME(UnfairLock.unlock(self:));
|
||||
OPENCOMBINE_SWIFT_NAME(__UnfairLock.unlock(self:));
|
||||
|
||||
void opencombine_unfair_lock_assert_owner(OpenCombineUnfairLock mutex)
|
||||
OPENCOMBINE_SWIFT_NAME(UnfairLock.assertOwner(self:));
|
||||
OPENCOMBINE_SWIFT_NAME(__UnfairLock.assertOwner(self:));
|
||||
|
||||
void opencombine_unfair_lock_dealloc(OpenCombineUnfairLock lock)
|
||||
OPENCOMBINE_SWIFT_NAME(UnfairLock.deallocate(self:));
|
||||
OPENCOMBINE_SWIFT_NAME(__UnfairLock.deallocate(self:));
|
||||
|
||||
#pragma mark - OpenCombineUnfairRecursiveLock
|
||||
|
||||
/// A wrapper around an opaque pointer for type safety in Swift.
|
||||
typedef struct OpenCombineUnfairRecursiveLock {
|
||||
void* _Nonnull opaque;
|
||||
} OPENCOMBINE_SWIFT_NAME(UnfairRecursiveLock) OpenCombineUnfairRecursiveLock;
|
||||
} OPENCOMBINE_SWIFT_NAME(__UnfairRecursiveLock) OpenCombineUnfairRecursiveLock;
|
||||
|
||||
OpenCombineUnfairRecursiveLock opencombine_unfair_recursive_lock_alloc(void)
|
||||
OPENCOMBINE_SWIFT_NAME(UnfairRecursiveLock.allocate());
|
||||
OPENCOMBINE_SWIFT_NAME(__UnfairRecursiveLock.allocate());
|
||||
|
||||
void opencombine_unfair_recursive_lock_lock(OpenCombineUnfairRecursiveLock)
|
||||
OPENCOMBINE_SWIFT_NAME(UnfairRecursiveLock.lock(self:));
|
||||
OPENCOMBINE_SWIFT_NAME(__UnfairRecursiveLock.lock(self:));
|
||||
|
||||
void opencombine_unfair_recursive_lock_unlock(OpenCombineUnfairRecursiveLock)
|
||||
OPENCOMBINE_SWIFT_NAME(UnfairRecursiveLock.unlock(self:));
|
||||
OPENCOMBINE_SWIFT_NAME(__UnfairRecursiveLock.unlock(self:));
|
||||
|
||||
void opencombine_unfair_recursive_lock_dealloc(OpenCombineUnfairRecursiveLock lock)
|
||||
OPENCOMBINE_SWIFT_NAME(UnfairRecursiveLock.deallocate(self:));
|
||||
OPENCOMBINE_SWIFT_NAME(__UnfairRecursiveLock.deallocate(self:));
|
||||
|
||||
#pragma mark - Breakpoint
|
||||
|
||||
OPENCOMBINE_ALWAYS_INLINE
|
||||
inline void opencombine_stop_in_debugger(void) OPENCOMBINE_SWIFT_NAME(__stopInDebugger());
|
||||
|
||||
void opencombine_stop_in_debugger(void) {
|
||||
raise(SIGTRAP);
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
} // extern "C"
|
||||
|
||||
@@ -5,14 +5,16 @@
|
||||
// Created by Sergej Jaskiewicz on 10.06.2019.
|
||||
//
|
||||
|
||||
import func COpenCombineHelpers.nextCombineIdentifier
|
||||
#if canImport(COpenCombineHelpers)
|
||||
import COpenCombineHelpers
|
||||
#endif
|
||||
|
||||
public struct CombineIdentifier: Hashable, CustomStringConvertible {
|
||||
|
||||
private let value: UInt64
|
||||
|
||||
public init() {
|
||||
value = nextCombineIdentifier()
|
||||
value = __nextCombineIdentifier()
|
||||
}
|
||||
|
||||
public init(_ obj: AnyObject) {
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
// Created by Sergej Jaskiewicz on 11.06.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
/// A subject that wraps a single value and publishes a new element whenever the value
|
||||
/// changes.
|
||||
public final class CurrentValueSubject<Output, Failure: Error>: Subject {
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
// Created by Max Desiatov on 24/11/2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
/// A publisher that eventually produces one value and then finishes or fails.
|
||||
public final class Future<Output, Failure>: Publisher where Failure: Error {
|
||||
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
// Created by Sergej Jaskiewicz on 23.10.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
/// A helper class that acts like both subscriber and subscription.
|
||||
///
|
||||
/// Filter-like operators send an instance of their `Inner` class that is subclass
|
||||
|
||||
@@ -5,7 +5,12 @@
|
||||
// Created by Sergej Jaskiewicz on 11.06.2019.
|
||||
//
|
||||
|
||||
#if canImport(COpenCombineHelpers)
|
||||
import COpenCombineHelpers
|
||||
#endif
|
||||
|
||||
internal typealias UnfairLock = __UnfairLock
|
||||
internal typealias UnfairRecursiveLock = __UnfairRecursiveLock
|
||||
|
||||
extension UnfairRecursiveLock {
|
||||
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
// Created by Sergej Jaskiewicz on 22.09.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
/// A helper class that acts like both subscriber and subscription.
|
||||
///
|
||||
/// Reduce-like operators send an instance of their `Inner` class that is subclass
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
// Created by Sergej Jaskiewicz on 16/09/2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
// NOTE: This class has been audited for thread safety.
|
||||
internal final class SubjectSubscriber<Downstream: Subject>
|
||||
: Subscriber,
|
||||
|
||||
@@ -7,8 +7,9 @@
|
||||
|
||||
/// A scheduler for performing synchronous actions.
|
||||
///
|
||||
/// You can use this scheduler for immediate actions. If you attempt to schedule
|
||||
/// actions after a specific date, the scheduler produces a fatal error.
|
||||
/// You can only use this scheduler for immediate actions. If you attempt to schedule
|
||||
/// actions after a specific date, this scheduler ignores the date and performs
|
||||
/// them immediately.
|
||||
public struct ImmediateScheduler: Scheduler {
|
||||
|
||||
/// The time type used by the immediate scheduler.
|
||||
|
||||
@@ -67,19 +67,126 @@ public final class ObservableObjectPublisher: Publisher {
|
||||
|
||||
public typealias Failure = Never
|
||||
|
||||
private let subject: PassthroughSubject<Void, Never>
|
||||
private let lock = UnfairLock.allocate()
|
||||
|
||||
public init() {
|
||||
subject = .init()
|
||||
private var connections = Set<Conduit>()
|
||||
|
||||
// TODO: Combine needs this for some reason
|
||||
private var identifier: ObjectIdentifier?
|
||||
|
||||
public init() {}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Input == Void, Downstream.Failure == Never
|
||||
{
|
||||
subject.subscribe(subscriber)
|
||||
let inner = Inner(downstream: subscriber, parent: self)
|
||||
lock.lock()
|
||||
connections.insert(inner)
|
||||
lock.unlock()
|
||||
subscriber.receive(subscription: inner)
|
||||
}
|
||||
|
||||
public func send() {
|
||||
subject.send()
|
||||
lock.lock()
|
||||
let connections = self.connections
|
||||
lock.unlock()
|
||||
for connection in connections {
|
||||
connection.send()
|
||||
}
|
||||
}
|
||||
|
||||
private func remove(_ conduit: Conduit) {
|
||||
lock.lock()
|
||||
connections.remove(conduit)
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
extension ObservableObjectPublisher {
|
||||
private class Conduit: Hashable {
|
||||
|
||||
fileprivate func send() {
|
||||
abstractMethod()
|
||||
}
|
||||
|
||||
fileprivate static func == (lhs: Conduit, rhs: Conduit) -> Bool {
|
||||
return lhs === rhs
|
||||
}
|
||||
|
||||
fileprivate func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(ObjectIdentifier(self))
|
||||
}
|
||||
}
|
||||
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Conduit,
|
||||
Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Void, Downstream.Failure == Never
|
||||
{
|
||||
private enum State {
|
||||
case initialized
|
||||
case active
|
||||
case terminal
|
||||
}
|
||||
|
||||
private weak var parent: ObservableObjectPublisher?
|
||||
private let downstream: Downstream
|
||||
private let downstreamLock = UnfairRecursiveLock.allocate()
|
||||
private let lock = UnfairLock.allocate()
|
||||
private var state = State.initialized
|
||||
|
||||
init(downstream: Downstream, parent: ObservableObjectPublisher) {
|
||||
self.parent = parent
|
||||
self.downstream = downstream
|
||||
}
|
||||
|
||||
deinit {
|
||||
downstreamLock.deallocate()
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
override func send() {
|
||||
lock.lock()
|
||||
let state = self.state
|
||||
lock.unlock()
|
||||
if state == .active {
|
||||
downstreamLock.lock()
|
||||
_ = downstream.receive()
|
||||
downstreamLock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
lock.lock()
|
||||
if state == .initialized {
|
||||
state = .active
|
||||
}
|
||||
lock.unlock()
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
state = .terminal
|
||||
lock.unlock()
|
||||
parent?.remove(self)
|
||||
}
|
||||
|
||||
var description: String { return "ObservableObjectPublisher" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
let children = CollectionOfOne<Mirror.Child>(("downstream", downstream))
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
var playgroundDescription: Any {
|
||||
return description
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
// Created by Sergej Jaskiewicz on 11.06.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
/// A subject that passes along values and completion.
|
||||
///
|
||||
/// Use a `PassthroughSubject` in unit tests when you want a publisher than can publish
|
||||
|
||||
@@ -0,0 +1,952 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ┃
|
||||
// ┃ Auto-generated from GYB template. DO NOT EDIT! ┃
|
||||
// ┃ ┃
|
||||
// ┃ ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
//
|
||||
// Publishers.Merge.swift.gyb
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 04/10/2019.
|
||||
//
|
||||
|
||||
// swiftlint:disable generic_type_name
|
||||
// swiftlint:disable vertical_parameter_alignment
|
||||
|
||||
// MARK: - Merge methods on Publisher
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Combines elements from this publisher with those from another publisher,
|
||||
/// delivering an interleaved sequence of elements.
|
||||
///
|
||||
/// The merged publisher continues to emit elements until all upstream publishers
|
||||
/// finish. If an upstream publisher produces an error, the merged publisher fails
|
||||
/// with that error.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - other: Another publisher.
|
||||
/// - Returns: A publisher that emits an event when any upstream publisher emits
|
||||
/// an event.
|
||||
public func merge<
|
||||
P: Publisher
|
||||
>(with other: P) -> Publishers.Merge<Self, P>
|
||||
where Failure == P.Failure, Output == P.Output
|
||||
{
|
||||
return .init(self, other)
|
||||
}
|
||||
/// Combines elements from this publisher with those from three other publishers,
|
||||
/// delivering an interleaved sequence of elements.
|
||||
///
|
||||
/// The merged publisher continues to emit elements until all upstream publishers
|
||||
/// finish. If an upstream publisher produces an error, the merged publisher fails
|
||||
/// with that error.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - b: A second publisher.
|
||||
/// - c: A third publisher.
|
||||
/// - Returns: A publisher that emits an event when any upstream publisher emits
|
||||
/// an event.
|
||||
public func merge<
|
||||
B: Publisher,
|
||||
C: Publisher
|
||||
>(with b: B,
|
||||
_ c: C) -> Publishers.Merge3<Self, B, C>
|
||||
where Failure == B.Failure, Output == B.Output,
|
||||
B.Failure == C.Failure, B.Output == C.Output
|
||||
{
|
||||
return .init(self, b, c)
|
||||
}
|
||||
/// Combines elements from this publisher with those from four other publishers,
|
||||
/// delivering an interleaved sequence of elements.
|
||||
///
|
||||
/// The merged publisher continues to emit elements until all upstream publishers
|
||||
/// finish. If an upstream publisher produces an error, the merged publisher fails
|
||||
/// with that error.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - b: A second publisher.
|
||||
/// - c: A third publisher.
|
||||
/// - d: A fourth publisher.
|
||||
/// - Returns: A publisher that emits an event when any upstream publisher emits
|
||||
/// an event.
|
||||
public func merge<
|
||||
B: Publisher,
|
||||
C: Publisher,
|
||||
D: Publisher
|
||||
>(with b: B,
|
||||
_ c: C,
|
||||
_ d: D) -> Publishers.Merge4<Self, B, C, D>
|
||||
where Failure == B.Failure, Output == B.Output,
|
||||
B.Failure == C.Failure, B.Output == C.Output,
|
||||
C.Failure == D.Failure, C.Output == D.Output
|
||||
{
|
||||
return .init(self, b, c, d)
|
||||
}
|
||||
/// Combines elements from this publisher with those from five other publishers,
|
||||
/// delivering an interleaved sequence of elements.
|
||||
///
|
||||
/// The merged publisher continues to emit elements until all upstream publishers
|
||||
/// finish. If an upstream publisher produces an error, the merged publisher fails
|
||||
/// with that error.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - b: A second publisher.
|
||||
/// - c: A third publisher.
|
||||
/// - d: A fourth publisher.
|
||||
/// - e: A fifth publisher.
|
||||
/// - Returns: A publisher that emits an event when any upstream publisher emits
|
||||
/// an event.
|
||||
public func merge<
|
||||
B: Publisher,
|
||||
C: Publisher,
|
||||
D: Publisher,
|
||||
E: Publisher
|
||||
>(with b: B,
|
||||
_ c: C,
|
||||
_ d: D,
|
||||
_ e: E) -> Publishers.Merge5<Self, B, C, D, E>
|
||||
where Failure == B.Failure, Output == B.Output,
|
||||
B.Failure == C.Failure, B.Output == C.Output,
|
||||
C.Failure == D.Failure, C.Output == D.Output,
|
||||
D.Failure == E.Failure, D.Output == E.Output
|
||||
{
|
||||
return .init(self, b, c, d, e)
|
||||
}
|
||||
/// Combines elements from this publisher with those from six other publishers,
|
||||
/// delivering an interleaved sequence of elements.
|
||||
///
|
||||
/// The merged publisher continues to emit elements until all upstream publishers
|
||||
/// finish. If an upstream publisher produces an error, the merged publisher fails
|
||||
/// with that error.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - b: A second publisher.
|
||||
/// - c: A third publisher.
|
||||
/// - d: A fourth publisher.
|
||||
/// - e: A fifth publisher.
|
||||
/// - f: A sixth publisher.
|
||||
/// - Returns: A publisher that emits an event when any upstream publisher emits
|
||||
/// an event.
|
||||
public func merge<
|
||||
B: Publisher,
|
||||
C: Publisher,
|
||||
D: Publisher,
|
||||
E: Publisher,
|
||||
F: Publisher
|
||||
>(with b: B,
|
||||
_ c: C,
|
||||
_ d: D,
|
||||
_ e: E,
|
||||
_ f: F) -> Publishers.Merge6<Self, B, C, D, E, F>
|
||||
where Failure == B.Failure, Output == B.Output,
|
||||
B.Failure == C.Failure, B.Output == C.Output,
|
||||
C.Failure == D.Failure, C.Output == D.Output,
|
||||
D.Failure == E.Failure, D.Output == E.Output,
|
||||
E.Failure == F.Failure, E.Output == F.Output
|
||||
{
|
||||
return .init(self, b, c, d, e, f)
|
||||
}
|
||||
/// Combines elements from this publisher with those from seven other publishers,
|
||||
/// delivering an interleaved sequence of elements.
|
||||
///
|
||||
/// The merged publisher continues to emit elements until all upstream publishers
|
||||
/// finish. If an upstream publisher produces an error, the merged publisher fails
|
||||
/// with that error.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - b: A second publisher.
|
||||
/// - c: A third publisher.
|
||||
/// - d: A fourth publisher.
|
||||
/// - e: A fifth publisher.
|
||||
/// - f: A sixth publisher.
|
||||
/// - g: A seventh publisher.
|
||||
/// - Returns: A publisher that emits an event when any upstream publisher emits
|
||||
/// an event.
|
||||
public func merge<
|
||||
B: Publisher,
|
||||
C: Publisher,
|
||||
D: Publisher,
|
||||
E: Publisher,
|
||||
F: Publisher,
|
||||
G: Publisher
|
||||
>(with b: B,
|
||||
_ c: C,
|
||||
_ d: D,
|
||||
_ e: E,
|
||||
_ f: F,
|
||||
_ g: G) -> Publishers.Merge7<Self, B, C, D, E, F, G>
|
||||
where Failure == B.Failure, Output == B.Output,
|
||||
B.Failure == C.Failure, B.Output == C.Output,
|
||||
C.Failure == D.Failure, C.Output == D.Output,
|
||||
D.Failure == E.Failure, D.Output == E.Output,
|
||||
E.Failure == F.Failure, E.Output == F.Output,
|
||||
F.Failure == G.Failure, F.Output == G.Output
|
||||
{
|
||||
return .init(self, b, c, d, e, f, g)
|
||||
}
|
||||
/// Combines elements from this publisher with those from eight other publishers,
|
||||
/// delivering an interleaved sequence of elements.
|
||||
///
|
||||
/// The merged publisher continues to emit elements until all upstream publishers
|
||||
/// finish. If an upstream publisher produces an error, the merged publisher fails
|
||||
/// with that error.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - b: A second publisher.
|
||||
/// - c: A third publisher.
|
||||
/// - d: A fourth publisher.
|
||||
/// - e: A fifth publisher.
|
||||
/// - f: A sixth publisher.
|
||||
/// - g: A seventh publisher.
|
||||
/// - h: An eighth publisher.
|
||||
/// - Returns: A publisher that emits an event when any upstream publisher emits
|
||||
/// an event.
|
||||
public func merge<
|
||||
B: Publisher,
|
||||
C: Publisher,
|
||||
D: Publisher,
|
||||
E: Publisher,
|
||||
F: Publisher,
|
||||
G: Publisher,
|
||||
H: Publisher
|
||||
>(with b: B,
|
||||
_ c: C,
|
||||
_ d: D,
|
||||
_ e: E,
|
||||
_ f: F,
|
||||
_ g: G,
|
||||
_ h: H) -> Publishers.Merge8<Self, B, C, D, E, F, G, H>
|
||||
where Failure == B.Failure, Output == B.Output,
|
||||
B.Failure == C.Failure, B.Output == C.Output,
|
||||
C.Failure == D.Failure, C.Output == D.Output,
|
||||
D.Failure == E.Failure, D.Output == E.Output,
|
||||
E.Failure == F.Failure, E.Output == F.Output,
|
||||
F.Failure == G.Failure, F.Output == G.Output,
|
||||
G.Failure == H.Failure, G.Output == H.Output
|
||||
{
|
||||
return .init(self, b, c, d, e, f, g, h)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Combines elements from this publisher with those from another publisher of
|
||||
/// the same type, delivering an interleaved sequence of elements.
|
||||
///
|
||||
/// - Parameter other: Another publisher of this publisher's type.
|
||||
/// - Returns: A publisher that emits an event when either upstream publisher emits
|
||||
/// an event.
|
||||
public func merge(with other: Self) -> Publishers.MergeMany<Self> {
|
||||
return .init([self, other])
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Merge publishers
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher created by applying the merge function to two upstream
|
||||
/// publishers.
|
||||
public struct Merge<A: Publisher,
|
||||
B: Publisher>: Publisher
|
||||
where A.Failure == B.Failure, A.Output == B.Output
|
||||
{
|
||||
public typealias Output = A.Output
|
||||
|
||||
public typealias Failure = A.Failure
|
||||
|
||||
public let a: A
|
||||
|
||||
public let b: B
|
||||
|
||||
public init(
|
||||
_ a: A,
|
||||
_ b: B
|
||||
) {
|
||||
self.a = a
|
||||
self.b = b
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where A.Failure == Downstream.Failure,
|
||||
A.Output == Downstream.Input
|
||||
{
|
||||
typealias Merged = _Merged<Output, Failure, Downstream>
|
||||
let merged = Merged(downstream: subscriber, count: 2)
|
||||
a.subscribe(Merged.Side(index: 0, merger: merged))
|
||||
b.subscribe(Merged.Side(index: 1, merger: merged))
|
||||
subscriber.receive(subscription: merged)
|
||||
}
|
||||
|
||||
public func merge<
|
||||
P: Publisher
|
||||
>(with other: P) -> Publishers.Merge3<A, B, P>
|
||||
{
|
||||
return .init(a, b, other)
|
||||
}
|
||||
|
||||
public func merge<
|
||||
Z: Publisher,
|
||||
Y: Publisher
|
||||
>(with z: Z,
|
||||
_ y: Y) -> Publishers.Merge4<A, B, Z, Y>
|
||||
{
|
||||
return .init(a, b, z, y)
|
||||
}
|
||||
|
||||
public func merge<
|
||||
Z: Publisher,
|
||||
Y: Publisher,
|
||||
X: Publisher
|
||||
>(with z: Z,
|
||||
_ y: Y,
|
||||
_ x: X) -> Publishers.Merge5<A, B, Z, Y, X>
|
||||
{
|
||||
return .init(a, b, z, y, x)
|
||||
}
|
||||
|
||||
public func merge<
|
||||
Z: Publisher,
|
||||
Y: Publisher,
|
||||
X: Publisher,
|
||||
W: Publisher
|
||||
>(with z: Z,
|
||||
_ y: Y,
|
||||
_ x: X,
|
||||
_ w: W) -> Publishers.Merge6<A, B, Z, Y, X, W>
|
||||
{
|
||||
return .init(a, b, z, y, x, w)
|
||||
}
|
||||
|
||||
public func merge<
|
||||
Z: Publisher,
|
||||
Y: Publisher,
|
||||
X: Publisher,
|
||||
W: Publisher,
|
||||
V: Publisher
|
||||
>(with z: Z,
|
||||
_ y: Y,
|
||||
_ x: X,
|
||||
_ w: W,
|
||||
_ v: V) -> Publishers.Merge7<A, B, Z, Y, X, W, V>
|
||||
{
|
||||
return .init(a, b, z, y, x, w, v)
|
||||
}
|
||||
|
||||
public func merge<
|
||||
Z: Publisher,
|
||||
Y: Publisher,
|
||||
X: Publisher,
|
||||
W: Publisher,
|
||||
V: Publisher,
|
||||
U: Publisher
|
||||
>(with z: Z,
|
||||
_ y: Y,
|
||||
_ x: X,
|
||||
_ w: W,
|
||||
_ v: V,
|
||||
_ u: U) -> Publishers.Merge8<A, B, Z, Y, X, W, V, U>
|
||||
{
|
||||
return .init(a, b, z, y, x, w, v, u)
|
||||
}
|
||||
}
|
||||
|
||||
/// A publisher created by applying the merge function to three upstream
|
||||
/// publishers.
|
||||
public struct Merge3<A: Publisher,
|
||||
B: Publisher,
|
||||
C: Publisher>: Publisher
|
||||
where A.Failure == B.Failure, A.Output == B.Output,
|
||||
B.Failure == C.Failure, B.Output == C.Output
|
||||
{
|
||||
public typealias Output = A.Output
|
||||
|
||||
public typealias Failure = A.Failure
|
||||
|
||||
public let a: A
|
||||
|
||||
public let b: B
|
||||
|
||||
public let c: C
|
||||
|
||||
public init(
|
||||
_ a: A,
|
||||
_ b: B,
|
||||
_ c: C
|
||||
) {
|
||||
self.a = a
|
||||
self.b = b
|
||||
self.c = c
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where A.Failure == Downstream.Failure,
|
||||
A.Output == Downstream.Input
|
||||
{
|
||||
typealias Merged = _Merged<Output, Failure, Downstream>
|
||||
let merged = Merged(downstream: subscriber, count: 3)
|
||||
a.subscribe(Merged.Side(index: 0, merger: merged))
|
||||
b.subscribe(Merged.Side(index: 1, merger: merged))
|
||||
c.subscribe(Merged.Side(index: 2, merger: merged))
|
||||
subscriber.receive(subscription: merged)
|
||||
}
|
||||
|
||||
public func merge<
|
||||
P: Publisher
|
||||
>(with other: P) -> Publishers.Merge4<A, B, C, P>
|
||||
{
|
||||
return .init(a, b, c, other)
|
||||
}
|
||||
|
||||
public func merge<
|
||||
Z: Publisher,
|
||||
Y: Publisher
|
||||
>(with z: Z,
|
||||
_ y: Y) -> Publishers.Merge5<A, B, C, Z, Y>
|
||||
{
|
||||
return .init(a, b, c, z, y)
|
||||
}
|
||||
|
||||
public func merge<
|
||||
Z: Publisher,
|
||||
Y: Publisher,
|
||||
X: Publisher
|
||||
>(with z: Z,
|
||||
_ y: Y,
|
||||
_ x: X) -> Publishers.Merge6<A, B, C, Z, Y, X>
|
||||
{
|
||||
return .init(a, b, c, z, y, x)
|
||||
}
|
||||
|
||||
public func merge<
|
||||
Z: Publisher,
|
||||
Y: Publisher,
|
||||
X: Publisher,
|
||||
W: Publisher
|
||||
>(with z: Z,
|
||||
_ y: Y,
|
||||
_ x: X,
|
||||
_ w: W) -> Publishers.Merge7<A, B, C, Z, Y, X, W>
|
||||
{
|
||||
return .init(a, b, c, z, y, x, w)
|
||||
}
|
||||
|
||||
public func merge<
|
||||
Z: Publisher,
|
||||
Y: Publisher,
|
||||
X: Publisher,
|
||||
W: Publisher,
|
||||
V: Publisher
|
||||
>(with z: Z,
|
||||
_ y: Y,
|
||||
_ x: X,
|
||||
_ w: W,
|
||||
_ v: V) -> Publishers.Merge8<A, B, C, Z, Y, X, W, V>
|
||||
{
|
||||
return .init(a, b, c, z, y, x, w, v)
|
||||
}
|
||||
}
|
||||
|
||||
/// A publisher created by applying the merge function to four upstream
|
||||
/// publishers.
|
||||
public struct Merge4<A: Publisher,
|
||||
B: Publisher,
|
||||
C: Publisher,
|
||||
D: Publisher>: Publisher
|
||||
where A.Failure == B.Failure, A.Output == B.Output,
|
||||
B.Failure == C.Failure, B.Output == C.Output,
|
||||
C.Failure == D.Failure, C.Output == D.Output
|
||||
{
|
||||
public typealias Output = A.Output
|
||||
|
||||
public typealias Failure = A.Failure
|
||||
|
||||
public let a: A
|
||||
|
||||
public let b: B
|
||||
|
||||
public let c: C
|
||||
|
||||
public let d: D
|
||||
|
||||
public init(
|
||||
_ a: A,
|
||||
_ b: B,
|
||||
_ c: C,
|
||||
_ d: D
|
||||
) {
|
||||
self.a = a
|
||||
self.b = b
|
||||
self.c = c
|
||||
self.d = d
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where A.Failure == Downstream.Failure,
|
||||
A.Output == Downstream.Input
|
||||
{
|
||||
typealias Merged = _Merged<Output, Failure, Downstream>
|
||||
let merged = Merged(downstream: subscriber, count: 4)
|
||||
a.subscribe(Merged.Side(index: 0, merger: merged))
|
||||
b.subscribe(Merged.Side(index: 1, merger: merged))
|
||||
c.subscribe(Merged.Side(index: 2, merger: merged))
|
||||
d.subscribe(Merged.Side(index: 3, merger: merged))
|
||||
subscriber.receive(subscription: merged)
|
||||
}
|
||||
|
||||
public func merge<
|
||||
P: Publisher
|
||||
>(with other: P) -> Publishers.Merge5<A, B, C, D, P>
|
||||
{
|
||||
return .init(a, b, c, d, other)
|
||||
}
|
||||
|
||||
public func merge<
|
||||
Z: Publisher,
|
||||
Y: Publisher
|
||||
>(with z: Z,
|
||||
_ y: Y) -> Publishers.Merge6<A, B, C, D, Z, Y>
|
||||
{
|
||||
return .init(a, b, c, d, z, y)
|
||||
}
|
||||
|
||||
public func merge<
|
||||
Z: Publisher,
|
||||
Y: Publisher,
|
||||
X: Publisher
|
||||
>(with z: Z,
|
||||
_ y: Y,
|
||||
_ x: X) -> Publishers.Merge7<A, B, C, D, Z, Y, X>
|
||||
{
|
||||
return .init(a, b, c, d, z, y, x)
|
||||
}
|
||||
|
||||
public func merge<
|
||||
Z: Publisher,
|
||||
Y: Publisher,
|
||||
X: Publisher,
|
||||
W: Publisher
|
||||
>(with z: Z,
|
||||
_ y: Y,
|
||||
_ x: X,
|
||||
_ w: W) -> Publishers.Merge8<A, B, C, D, Z, Y, X, W>
|
||||
{
|
||||
return .init(a, b, c, d, z, y, x, w)
|
||||
}
|
||||
}
|
||||
|
||||
/// A publisher created by applying the merge function to five upstream
|
||||
/// publishers.
|
||||
public struct Merge5<A: Publisher,
|
||||
B: Publisher,
|
||||
C: Publisher,
|
||||
D: Publisher,
|
||||
E: Publisher>: Publisher
|
||||
where A.Failure == B.Failure, A.Output == B.Output,
|
||||
B.Failure == C.Failure, B.Output == C.Output,
|
||||
C.Failure == D.Failure, C.Output == D.Output,
|
||||
D.Failure == E.Failure, D.Output == E.Output
|
||||
{
|
||||
public typealias Output = A.Output
|
||||
|
||||
public typealias Failure = A.Failure
|
||||
|
||||
public let a: A
|
||||
|
||||
public let b: B
|
||||
|
||||
public let c: C
|
||||
|
||||
public let d: D
|
||||
|
||||
public let e: E
|
||||
|
||||
public init(
|
||||
_ a: A,
|
||||
_ b: B,
|
||||
_ c: C,
|
||||
_ d: D,
|
||||
_ e: E
|
||||
) {
|
||||
self.a = a
|
||||
self.b = b
|
||||
self.c = c
|
||||
self.d = d
|
||||
self.e = e
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where A.Failure == Downstream.Failure,
|
||||
A.Output == Downstream.Input
|
||||
{
|
||||
typealias Merged = _Merged<Output, Failure, Downstream>
|
||||
let merged = Merged(downstream: subscriber, count: 5)
|
||||
a.subscribe(Merged.Side(index: 0, merger: merged))
|
||||
b.subscribe(Merged.Side(index: 1, merger: merged))
|
||||
c.subscribe(Merged.Side(index: 2, merger: merged))
|
||||
d.subscribe(Merged.Side(index: 3, merger: merged))
|
||||
e.subscribe(Merged.Side(index: 4, merger: merged))
|
||||
subscriber.receive(subscription: merged)
|
||||
}
|
||||
|
||||
public func merge<
|
||||
P: Publisher
|
||||
>(with other: P) -> Publishers.Merge6<A, B, C, D, E, P>
|
||||
{
|
||||
return .init(a, b, c, d, e, other)
|
||||
}
|
||||
|
||||
public func merge<
|
||||
Z: Publisher,
|
||||
Y: Publisher
|
||||
>(with z: Z,
|
||||
_ y: Y) -> Publishers.Merge7<A, B, C, D, E, Z, Y>
|
||||
{
|
||||
return .init(a, b, c, d, e, z, y)
|
||||
}
|
||||
|
||||
public func merge<
|
||||
Z: Publisher,
|
||||
Y: Publisher,
|
||||
X: Publisher
|
||||
>(with z: Z,
|
||||
_ y: Y,
|
||||
_ x: X) -> Publishers.Merge8<A, B, C, D, E, Z, Y, X>
|
||||
{
|
||||
return .init(a, b, c, d, e, z, y, x)
|
||||
}
|
||||
}
|
||||
|
||||
/// A publisher created by applying the merge function to six upstream
|
||||
/// publishers.
|
||||
public struct Merge6<A: Publisher,
|
||||
B: Publisher,
|
||||
C: Publisher,
|
||||
D: Publisher,
|
||||
E: Publisher,
|
||||
F: Publisher>: Publisher
|
||||
where A.Failure == B.Failure, A.Output == B.Output,
|
||||
B.Failure == C.Failure, B.Output == C.Output,
|
||||
C.Failure == D.Failure, C.Output == D.Output,
|
||||
D.Failure == E.Failure, D.Output == E.Output,
|
||||
E.Failure == F.Failure, E.Output == F.Output
|
||||
{
|
||||
public typealias Output = A.Output
|
||||
|
||||
public typealias Failure = A.Failure
|
||||
|
||||
public let a: A
|
||||
|
||||
public let b: B
|
||||
|
||||
public let c: C
|
||||
|
||||
public let d: D
|
||||
|
||||
public let e: E
|
||||
|
||||
public let f: F
|
||||
|
||||
public init(
|
||||
_ a: A,
|
||||
_ b: B,
|
||||
_ c: C,
|
||||
_ d: D,
|
||||
_ e: E,
|
||||
_ f: F
|
||||
) {
|
||||
self.a = a
|
||||
self.b = b
|
||||
self.c = c
|
||||
self.d = d
|
||||
self.e = e
|
||||
self.f = f
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where A.Failure == Downstream.Failure,
|
||||
A.Output == Downstream.Input
|
||||
{
|
||||
typealias Merged = _Merged<Output, Failure, Downstream>
|
||||
let merged = Merged(downstream: subscriber, count: 6)
|
||||
a.subscribe(Merged.Side(index: 0, merger: merged))
|
||||
b.subscribe(Merged.Side(index: 1, merger: merged))
|
||||
c.subscribe(Merged.Side(index: 2, merger: merged))
|
||||
d.subscribe(Merged.Side(index: 3, merger: merged))
|
||||
e.subscribe(Merged.Side(index: 4, merger: merged))
|
||||
f.subscribe(Merged.Side(index: 5, merger: merged))
|
||||
subscriber.receive(subscription: merged)
|
||||
}
|
||||
|
||||
public func merge<
|
||||
P: Publisher
|
||||
>(with other: P) -> Publishers.Merge7<A, B, C, D, E, F, P>
|
||||
{
|
||||
return .init(a, b, c, d, e, f, other)
|
||||
}
|
||||
|
||||
public func merge<
|
||||
Z: Publisher,
|
||||
Y: Publisher
|
||||
>(with z: Z,
|
||||
_ y: Y) -> Publishers.Merge8<A, B, C, D, E, F, Z, Y>
|
||||
{
|
||||
return .init(a, b, c, d, e, f, z, y)
|
||||
}
|
||||
}
|
||||
|
||||
/// A publisher created by applying the merge function to seven upstream
|
||||
/// publishers.
|
||||
public struct Merge7<A: Publisher,
|
||||
B: Publisher,
|
||||
C: Publisher,
|
||||
D: Publisher,
|
||||
E: Publisher,
|
||||
F: Publisher,
|
||||
G: Publisher>: Publisher
|
||||
where A.Failure == B.Failure, A.Output == B.Output,
|
||||
B.Failure == C.Failure, B.Output == C.Output,
|
||||
C.Failure == D.Failure, C.Output == D.Output,
|
||||
D.Failure == E.Failure, D.Output == E.Output,
|
||||
E.Failure == F.Failure, E.Output == F.Output,
|
||||
F.Failure == G.Failure, F.Output == G.Output
|
||||
{
|
||||
public typealias Output = A.Output
|
||||
|
||||
public typealias Failure = A.Failure
|
||||
|
||||
public let a: A
|
||||
|
||||
public let b: B
|
||||
|
||||
public let c: C
|
||||
|
||||
public let d: D
|
||||
|
||||
public let e: E
|
||||
|
||||
public let f: F
|
||||
|
||||
public let g: G
|
||||
|
||||
public init(
|
||||
_ a: A,
|
||||
_ b: B,
|
||||
_ c: C,
|
||||
_ d: D,
|
||||
_ e: E,
|
||||
_ f: F,
|
||||
_ g: G
|
||||
) {
|
||||
self.a = a
|
||||
self.b = b
|
||||
self.c = c
|
||||
self.d = d
|
||||
self.e = e
|
||||
self.f = f
|
||||
self.g = g
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where A.Failure == Downstream.Failure,
|
||||
A.Output == Downstream.Input
|
||||
{
|
||||
typealias Merged = _Merged<Output, Failure, Downstream>
|
||||
let merged = Merged(downstream: subscriber, count: 7)
|
||||
a.subscribe(Merged.Side(index: 0, merger: merged))
|
||||
b.subscribe(Merged.Side(index: 1, merger: merged))
|
||||
c.subscribe(Merged.Side(index: 2, merger: merged))
|
||||
d.subscribe(Merged.Side(index: 3, merger: merged))
|
||||
e.subscribe(Merged.Side(index: 4, merger: merged))
|
||||
f.subscribe(Merged.Side(index: 5, merger: merged))
|
||||
g.subscribe(Merged.Side(index: 6, merger: merged))
|
||||
subscriber.receive(subscription: merged)
|
||||
}
|
||||
|
||||
public func merge<
|
||||
P: Publisher
|
||||
>(with other: P) -> Publishers.Merge8<A, B, C, D, E, F, G, P>
|
||||
{
|
||||
return .init(a, b, c, d, e, f, g, other)
|
||||
}
|
||||
}
|
||||
|
||||
/// A publisher created by applying the merge function to eight upstream
|
||||
/// publishers.
|
||||
public struct Merge8<A: Publisher,
|
||||
B: Publisher,
|
||||
C: Publisher,
|
||||
D: Publisher,
|
||||
E: Publisher,
|
||||
F: Publisher,
|
||||
G: Publisher,
|
||||
H: Publisher>: Publisher
|
||||
where A.Failure == B.Failure, A.Output == B.Output,
|
||||
B.Failure == C.Failure, B.Output == C.Output,
|
||||
C.Failure == D.Failure, C.Output == D.Output,
|
||||
D.Failure == E.Failure, D.Output == E.Output,
|
||||
E.Failure == F.Failure, E.Output == F.Output,
|
||||
F.Failure == G.Failure, F.Output == G.Output,
|
||||
G.Failure == H.Failure, G.Output == H.Output
|
||||
{
|
||||
public typealias Output = A.Output
|
||||
|
||||
public typealias Failure = A.Failure
|
||||
|
||||
public let a: A
|
||||
|
||||
public let b: B
|
||||
|
||||
public let c: C
|
||||
|
||||
public let d: D
|
||||
|
||||
public let e: E
|
||||
|
||||
public let f: F
|
||||
|
||||
public let g: G
|
||||
|
||||
public let h: H
|
||||
|
||||
public init(
|
||||
_ a: A,
|
||||
_ b: B,
|
||||
_ c: C,
|
||||
_ d: D,
|
||||
_ e: E,
|
||||
_ f: F,
|
||||
_ g: G,
|
||||
_ h: H
|
||||
) {
|
||||
self.a = a
|
||||
self.b = b
|
||||
self.c = c
|
||||
self.d = d
|
||||
self.e = e
|
||||
self.f = f
|
||||
self.g = g
|
||||
self.h = h
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where A.Failure == Downstream.Failure,
|
||||
A.Output == Downstream.Input
|
||||
{
|
||||
typealias Merged = _Merged<Output, Failure, Downstream>
|
||||
let merged = Merged(downstream: subscriber, count: 8)
|
||||
a.subscribe(Merged.Side(index: 0, merger: merged))
|
||||
b.subscribe(Merged.Side(index: 1, merger: merged))
|
||||
c.subscribe(Merged.Side(index: 2, merger: merged))
|
||||
d.subscribe(Merged.Side(index: 3, merger: merged))
|
||||
e.subscribe(Merged.Side(index: 4, merger: merged))
|
||||
f.subscribe(Merged.Side(index: 5, merger: merged))
|
||||
g.subscribe(Merged.Side(index: 6, merger: merged))
|
||||
h.subscribe(Merged.Side(index: 7, merger: merged))
|
||||
subscriber.receive(subscription: merged)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
public struct MergeMany<Upstream: Publisher>: Publisher {
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
public let publishers: [Upstream]
|
||||
|
||||
public init(_ upstream: Upstream...) {
|
||||
self.publishers = upstream
|
||||
}
|
||||
|
||||
public init<UpstreamPublishers: Swift.Sequence>(_ upstream: UpstreamPublishers)
|
||||
where Upstream == UpstreamPublishers.Element
|
||||
{
|
||||
publishers = Array(upstream)
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Failure == Downstream.Failure,
|
||||
Upstream.Output == Downstream.Input
|
||||
{
|
||||
typealias Merged = _Merged<Output, Failure, Downstream>
|
||||
let merged = Merged(downstream: subscriber, count: publishers.count)
|
||||
for (i, upstream) in publishers.enumerated() {
|
||||
upstream.subscribe(Merged.Side(index: i, merger: merged))
|
||||
}
|
||||
subscriber.receive(subscription: merged)
|
||||
}
|
||||
|
||||
public func merge(with other: Upstream) -> Publishers.MergeMany<Upstream> {
|
||||
var newPublishers = publishers
|
||||
newPublishers.append(other)
|
||||
return .init(newPublishers)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Equatable conformances
|
||||
|
||||
extension Publishers.Merge: Equatable
|
||||
where
|
||||
A: Equatable,
|
||||
B: Equatable {}
|
||||
|
||||
extension Publishers.Merge3: Equatable
|
||||
where
|
||||
A: Equatable,
|
||||
B: Equatable,
|
||||
C: Equatable {}
|
||||
|
||||
extension Publishers.Merge4: Equatable
|
||||
where
|
||||
A: Equatable,
|
||||
B: Equatable,
|
||||
C: Equatable,
|
||||
D: Equatable {}
|
||||
|
||||
extension Publishers.Merge5: Equatable
|
||||
where
|
||||
A: Equatable,
|
||||
B: Equatable,
|
||||
C: Equatable,
|
||||
D: Equatable,
|
||||
E: Equatable {}
|
||||
|
||||
extension Publishers.Merge6: Equatable
|
||||
where
|
||||
A: Equatable,
|
||||
B: Equatable,
|
||||
C: Equatable,
|
||||
D: Equatable,
|
||||
E: Equatable,
|
||||
F: Equatable {}
|
||||
|
||||
extension Publishers.Merge7: Equatable
|
||||
where
|
||||
A: Equatable,
|
||||
B: Equatable,
|
||||
C: Equatable,
|
||||
D: Equatable,
|
||||
E: Equatable,
|
||||
F: Equatable,
|
||||
G: Equatable {}
|
||||
|
||||
extension Publishers.Merge8: Equatable
|
||||
where
|
||||
A: Equatable,
|
||||
B: Equatable,
|
||||
C: Equatable,
|
||||
D: Equatable,
|
||||
E: Equatable,
|
||||
F: Equatable,
|
||||
G: Equatable,
|
||||
H: Equatable {}
|
||||
|
||||
extension Publishers.MergeMany: Equatable
|
||||
where
|
||||
Upstream: Equatable {}
|
||||
@@ -249,6 +249,26 @@ extension Just {
|
||||
) -> Result<ElementOfResult, Error>.OCombine.Publisher {
|
||||
return .init(Result { try nextPartialResult(initialResult, output) })
|
||||
}
|
||||
|
||||
public func prepend(_ elements: Output...) -> Publishers.Sequence<[Output], Never> {
|
||||
return prepend(elements)
|
||||
}
|
||||
|
||||
public func prepend<Elements: Sequence>(
|
||||
_ elements: Elements
|
||||
) -> Publishers.Sequence<[Output], Never> where Output == Elements.Element {
|
||||
return .init(sequence: elements + [output])
|
||||
}
|
||||
|
||||
public func append(_ elements: Output...) -> Publishers.Sequence<[Output], Never> {
|
||||
return append(elements)
|
||||
}
|
||||
|
||||
public func append<Elements: Sequence>(
|
||||
_ elements: Elements
|
||||
) -> Publishers.Sequence<[Output], Never> where Output == Elements.Element {
|
||||
return .init(sequence: [output] + elements)
|
||||
}
|
||||
}
|
||||
|
||||
extension Just {
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
//
|
||||
// Publishers.AssertNoFailure.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 25.12.2019.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Raises a fatal error when its upstream publisher fails, and otherwise republishes
|
||||
/// all received input.
|
||||
///
|
||||
/// Use this function for internal sanity checks that are active during testing but
|
||||
/// do not impact performance of shipping code.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - prefix: A string used at the beginning of the fatal error message.
|
||||
/// - file: A filename used in the error message. This defaults to `#file`.
|
||||
/// - line: A line number used in the error message. This defaults to `#line`.
|
||||
/// - Returns: A publisher that raises a fatal error when its upstream publisher
|
||||
/// fails.
|
||||
public func assertNoFailure(_ prefix: String = "",
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line) -> Publishers.AssertNoFailure<Self> {
|
||||
return .init(upstream: self, prefix: prefix, file: file, line: line)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that raises a fatal error upon receiving any failure, and otherwise
|
||||
/// republishes all received input.
|
||||
///
|
||||
/// Use this function for internal sanity checks that are active during testing but
|
||||
/// do not impact performance of shipping code.
|
||||
public struct AssertNoFailure<Upstream: Publisher>: Publisher {
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Never
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The string used at the beginning of the fatal error message.
|
||||
public let prefix: String
|
||||
|
||||
/// The filename used in the error message.
|
||||
public let file: StaticString
|
||||
|
||||
/// The line number used in the error message.
|
||||
public let line: UInt
|
||||
|
||||
public init(upstream: Upstream, prefix: String, file: StaticString, line: UInt) {
|
||||
self.upstream = upstream
|
||||
self.prefix = prefix
|
||||
self.file = file
|
||||
self.line = line
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Input == Output, Downstream.Failure == Never
|
||||
{
|
||||
upstream.subscribe(Inner(downstream: subscriber,
|
||||
prefix: prefix,
|
||||
file: file,
|
||||
line: line))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.AssertNoFailure {
|
||||
private struct Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Upstream.Output, Downstream.Failure == Never
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
private let downstream: Downstream
|
||||
|
||||
private let prefix: String
|
||||
|
||||
private let file: StaticString
|
||||
|
||||
private let line: UInt
|
||||
|
||||
let combineIdentifier = CombineIdentifier()
|
||||
|
||||
init(downstream: Downstream, prefix: String, file: StaticString, line: UInt) {
|
||||
self.downstream = downstream
|
||||
self.prefix = prefix
|
||||
self.file = file
|
||||
self.line = line
|
||||
}
|
||||
|
||||
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):
|
||||
let prefix = self.prefix.isEmpty ? "" : self.prefix + ": "
|
||||
fatalError("\(prefix)\(error)", file: file, line: line)
|
||||
}
|
||||
}
|
||||
|
||||
var description: String { return "AssertNoFailure" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
let children: [Mirror.Child] = [
|
||||
("file", file),
|
||||
("line", line),
|
||||
("prefix", prefix)
|
||||
]
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,6 @@
|
||||
// Created by Sergej Jaskiewicz on 18/09/2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
extension ConnectablePublisher {
|
||||
|
||||
/// Automates the process of connecting or disconnecting from this connectable
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
//
|
||||
// Publishers.Breakpoint.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 03.12.2019.
|
||||
//
|
||||
|
||||
#if canImport(COpenCombineHelpers)
|
||||
import COpenCombineHelpers
|
||||
#endif
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Raises a debugger signal when a provided closure needs to stop the process in
|
||||
/// the debugger.
|
||||
///
|
||||
/// When any of the provided closures returns `true`, this publisher raises
|
||||
/// the `SIGTRAP` signal to stop the process in the debugger.
|
||||
/// Otherwise, this publisher passes through values and completions as-is.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - receiveSubscription: A closure that executes when when the publisher receives
|
||||
/// a subscription. Return `true` from this closure to raise `SIGTRAP`, or `false`
|
||||
/// to continue.
|
||||
/// - receiveOutput: A closure that executes when when the publisher receives
|
||||
/// a value. Return `true` from this closure to raise `SIGTRAP`, or `false`
|
||||
/// to continue.
|
||||
/// - receiveCompletion: A closure that executes when when the publisher receives
|
||||
/// a completion. Return `true` from this closure to raise `SIGTRAP`, or `false`
|
||||
/// to continue.
|
||||
/// - Returns: A publisher that raises a debugger signal when one of the provided
|
||||
/// closures returns `true`.
|
||||
public func breakpoint(
|
||||
receiveSubscription: ((Subscription) -> Bool)? = nil,
|
||||
receiveOutput: ((Output) -> Bool)? = nil,
|
||||
receiveCompletion: ((Subscribers.Completion<Failure>) -> Bool)? = nil
|
||||
) -> Publishers.Breakpoint<Self> {
|
||||
return .init(upstream: self,
|
||||
receiveSubscription: receiveSubscription,
|
||||
receiveOutput: receiveOutput,
|
||||
receiveCompletion: receiveCompletion)
|
||||
}
|
||||
|
||||
/// Raises a debugger signal upon receiving a failure.
|
||||
///
|
||||
/// When the upstream publisher fails with an error, this publisher raises
|
||||
/// the `SIGTRAP` signal, which stops the process in the debugger.
|
||||
/// Otherwise, this publisher passes through values and completions as-is.
|
||||
///
|
||||
/// - Returns: A publisher that raises a debugger signal upon receiving a failure.
|
||||
public func breakpointOnError() -> Publishers.Breakpoint<Self> {
|
||||
return breakpoint { completion in
|
||||
switch completion {
|
||||
case .finished:
|
||||
return false
|
||||
case .failure:
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that raises a debugger signal when a provided closure needs to stop
|
||||
/// the process in the debugger.
|
||||
///
|
||||
/// When any of the provided closures returns `true`, this publisher raises
|
||||
/// the `SIGTRAP` signal to stop the process in the debugger.
|
||||
/// Otherwise, this publisher passes through values and completions as-is.
|
||||
public struct Breakpoint<Upstream: Publisher>: Publisher {
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// A closure that executes when the publisher receives a subscription, and can
|
||||
/// raise a debugger signal by returning a `true` Boolean value.
|
||||
public let receiveSubscription: ((Subscription) -> Bool)?
|
||||
|
||||
/// A closure that executes when the publisher receives output from the upstream
|
||||
/// publisher, and can raise a debugger signal by returning a `true` Boolean
|
||||
/// value.
|
||||
public let receiveOutput: ((Upstream.Output) -> Bool)?
|
||||
|
||||
/// A closure that executes when the publisher receives completion, and can raise
|
||||
/// a debugger signal by returning a `true` Boolean value.
|
||||
public let receiveCompletion:
|
||||
((Subscribers.Completion<Upstream.Failure>) -> Bool)?
|
||||
|
||||
/// Creates a breakpoint publisher with the provided upstream publisher and
|
||||
/// breakpoint-raising closures.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - upstream: The publisher from which this publisher receives elements.
|
||||
/// - receiveSubscription: A closure that executes when the publisher receives
|
||||
/// a subscription, and can raise a debugger signal by returning a `true`
|
||||
/// Boolean value.
|
||||
/// - receiveOutput: A closure that executes when the publisher receives output
|
||||
/// from the upstream publisher, and can raise a debugger signal by returning
|
||||
/// a `true` Boolean value.
|
||||
/// - receiveCompletion: A closure that executes when the publisher receives
|
||||
/// completion, and can raise a debugger signal by returning a `true` Boolean
|
||||
/// value.
|
||||
public init(
|
||||
upstream: Upstream,
|
||||
receiveSubscription: ((Subscription) -> Bool)? = nil,
|
||||
receiveOutput: ((Upstream.Output) -> Bool)? = nil,
|
||||
receiveCompletion: ((Subscribers.Completion<Failure>) -> Bool)? = nil
|
||||
) {
|
||||
self.upstream = upstream
|
||||
self.receiveSubscription = receiveSubscription
|
||||
self.receiveOutput = receiveOutput
|
||||
self.receiveCompletion = receiveCompletion
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Failure == Downstream.Failure,
|
||||
Upstream.Output == Downstream.Input
|
||||
{
|
||||
upstream.subscribe(Inner(self, downstream: subscriber))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.Breakpoint {
|
||||
private struct Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Upstream.Output, Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
private let downstream: Downstream
|
||||
private let breakpoint: Publishers.Breakpoint<Upstream>
|
||||
|
||||
let combineIdentifier = CombineIdentifier()
|
||||
|
||||
init(_ breakpoint: Publishers.Breakpoint<Upstream>,
|
||||
downstream: Downstream) {
|
||||
self.downstream = downstream
|
||||
self.breakpoint = breakpoint
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
if breakpoint.receiveSubscription?(subscription) == true {
|
||||
__stopInDebugger()
|
||||
}
|
||||
downstream.receive(subscription: subscription)
|
||||
}
|
||||
|
||||
func receive(_ input: Upstream.Output) -> Subscribers.Demand {
|
||||
if breakpoint.receiveOutput?(input) == true {
|
||||
__stopInDebugger()
|
||||
}
|
||||
return downstream.receive(input)
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Upstream.Failure>) {
|
||||
if breakpoint.receiveCompletion?(completion) == true {
|
||||
__stopInDebugger()
|
||||
}
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
|
||||
var description: String { return "Breakpoint" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
let children = CollectionOfOne<Mirror.Child>(
|
||||
("upstream", breakpoint.upstream)
|
||||
)
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
//
|
||||
// Publishers.CollectByCount.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 24.12.2019.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Collects up to the specified number of elements, and then emits a single array of
|
||||
/// the collection.
|
||||
///
|
||||
/// If the upstream publisher finishes before filling the buffer, this publisher sends
|
||||
/// an array of all the items it has received. This may be fewer than `count`
|
||||
/// elements.
|
||||
/// If the upstream publisher fails with an error, this publisher forwards the error
|
||||
/// to the downstream receiver instead of sending its output.
|
||||
/// Note: When this publisher receives a request for `.max(n)` elements, it requests
|
||||
/// `.max(count * n)` from the upstream publisher.
|
||||
///
|
||||
/// - Parameter count: The maximum number of received elements to buffer before
|
||||
/// publishing.
|
||||
/// - Returns: A publisher that collects up to the specified number of elements, and
|
||||
/// then publishes them as an array.
|
||||
public func collect(_ count: Int) -> Publishers.CollectByCount<Self> {
|
||||
return .init(upstream: self, count: count)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that buffers a maximum number of items.
|
||||
public struct CollectByCount<Upstream: Publisher>: Publisher {
|
||||
|
||||
public typealias Output = [Upstream.Output]
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The maximum number of received elements to buffer before publishing.
|
||||
public let count: Int
|
||||
|
||||
public init(upstream: Upstream, count: Int) {
|
||||
self.upstream = upstream
|
||||
self.count = count
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Failure == Failure, Downstream.Input == Output
|
||||
{
|
||||
upstream.subscribe(Inner(downstream: subscriber, count: count))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.CollectByCount: Equatable where Upstream: Equatable {}
|
||||
|
||||
extension Publishers.CollectByCount {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == [Upstream.Output],
|
||||
Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
private let downstream: Downstream
|
||||
|
||||
private let count: Int
|
||||
|
||||
private var buffer: [Input] = []
|
||||
|
||||
private var subscription: Subscription?
|
||||
|
||||
private var finished = false
|
||||
|
||||
private let lock = UnfairLock.allocate()
|
||||
|
||||
init(downstream: Downstream, count: Int) {
|
||||
self.downstream = downstream
|
||||
self.count = count
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
if finished || self.subscription != nil {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
self.subscription = subscription
|
||||
lock.unlock()
|
||||
downstream.receive(subscription: self)
|
||||
}
|
||||
|
||||
func receive(_ input: Upstream.Output) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
if subscription == nil {
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
buffer.append(input)
|
||||
guard buffer.count == count else {
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
let output = self.buffer
|
||||
self.buffer = []
|
||||
lock.unlock()
|
||||
return downstream.receive(output) * count
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Upstream.Failure>) {
|
||||
lock.lock()
|
||||
subscription = nil
|
||||
finished = true
|
||||
switch completion {
|
||||
case .finished:
|
||||
if buffer.isEmpty {
|
||||
lock.unlock()
|
||||
} else {
|
||||
let buffer = self.buffer
|
||||
self.buffer = []
|
||||
lock.unlock()
|
||||
_ = downstream.receive(buffer)
|
||||
}
|
||||
case .failure:
|
||||
buffer = []
|
||||
lock.unlock()
|
||||
}
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
demand.assertNonZero()
|
||||
lock.lock()
|
||||
if let subscription = self.subscription {
|
||||
lock.unlock()
|
||||
subscription.request(demand * count)
|
||||
} else {
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
if let subscription = self.subscription {
|
||||
buffer = []
|
||||
finished = true
|
||||
self.subscription = nil
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
} else {
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
var description: String { return "CollectByCount" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
let children: [Mirror.Child] = [
|
||||
("downstream", downstream),
|
||||
("upstreamSubscription", subscription as Any),
|
||||
("buffer", buffer),
|
||||
("count", count)
|
||||
]
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
//
|
||||
// Publishers.Concatenate.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 24.10.2019.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Prefixes a `Publisher`'s output with the specified sequence.
|
||||
///
|
||||
/// - Parameter elements: The elements to publish before this publisher’s elements.
|
||||
/// - Returns: A publisher that prefixes the specified elements prior to this
|
||||
/// publisher’s elements.
|
||||
public func prepend(
|
||||
_ elements: Output...
|
||||
) -> Publishers.Concatenate<Publishers.Sequence<[Output], Failure>, Self> {
|
||||
return prepend(elements)
|
||||
}
|
||||
|
||||
/// Prefixes a `Publisher`'s output with the specified sequence.
|
||||
///
|
||||
/// - Parameter elements: A sequence of elements to publish before this publisher’s
|
||||
/// elements.
|
||||
/// - Returns: A publisher that prefixes the sequence of elements prior to this
|
||||
/// publisher’s elements.
|
||||
public func prepend<Elements: Sequence>(
|
||||
_ elements: Elements
|
||||
) -> Publishers.Concatenate<Publishers.Sequence<Elements, Failure>, Self>
|
||||
where Output == Elements.Element
|
||||
{
|
||||
return prepend(.init(sequence: elements))
|
||||
}
|
||||
|
||||
/// Prefixes this publisher’s output with the elements emitted by the given publisher.
|
||||
///
|
||||
/// The resulting publisher doesn’t emit any elements until the prefixing publisher
|
||||
/// finishes.
|
||||
///
|
||||
/// - Parameter publisher: The prefixing publisher.
|
||||
/// - Returns: A publisher that prefixes the prefixing publisher’s elements prior to
|
||||
/// this publisher’s elements.
|
||||
public func prepend<Prefix: Publisher>(
|
||||
_ publisher: Prefix
|
||||
) -> Publishers.Concatenate<Prefix, Self>
|
||||
where Failure == Prefix.Failure, Output == Prefix.Output
|
||||
{
|
||||
return .init(prefix: publisher, suffix: self)
|
||||
}
|
||||
|
||||
/// Append a `Publisher`'s output with the specified sequence.
|
||||
public func append(
|
||||
_ elements: Output...
|
||||
) -> Publishers.Concatenate<Self, Publishers.Sequence<[Output], Failure>> {
|
||||
return append(elements)
|
||||
}
|
||||
|
||||
/// Appends a `Publisher`'s output with the specified sequence.
|
||||
public func append<Elements: Sequence>(
|
||||
_ elements: Elements
|
||||
) -> Publishers.Concatenate<Self, Publishers.Sequence<Elements, Failure>>
|
||||
where Output == Elements.Element
|
||||
{
|
||||
return append(.init(sequence: elements))
|
||||
}
|
||||
|
||||
/// Appends this publisher’s output with the elements emitted by the given publisher.
|
||||
///
|
||||
/// This operator produces no elements until this publisher finishes. It then produces
|
||||
/// this publisher’s elements, followed by the given publisher’s elements.
|
||||
/// If this publisher fails with an error, the prefixing publisher does not publish
|
||||
/// the provided publisher’s elements.
|
||||
///
|
||||
/// - Parameter publisher: The appending publisher.
|
||||
/// - Returns: A publisher that appends the appending publisher’s elements after this
|
||||
/// publisher’s elements.
|
||||
public func append<Suffix: Publisher>(
|
||||
_ publisher: Suffix
|
||||
) -> Publishers.Concatenate<Self, Suffix>
|
||||
where Suffix.Failure == Failure, Suffix.Output == Output
|
||||
{
|
||||
return .init(prefix: self, suffix: publisher)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that emits all of one publisher’s elements before those from anothe
|
||||
/// publisher.
|
||||
public struct Concatenate<Prefix: Publisher, Suffix: Publisher>: Publisher
|
||||
where Prefix.Failure == Suffix.Failure, Prefix.Output == Suffix.Output
|
||||
{
|
||||
public typealias Output = Suffix.Output
|
||||
|
||||
public typealias Failure = Suffix.Failure
|
||||
|
||||
/// The publisher to republish, in its entirety, before republishing elements from
|
||||
/// `suffix`.
|
||||
public let prefix: Prefix
|
||||
|
||||
/// The publisher to republish only after `prefix` finishes.
|
||||
public let suffix: Suffix
|
||||
|
||||
public init(prefix: Prefix, suffix: Suffix) {
|
||||
self.prefix = prefix
|
||||
self.suffix = suffix
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Suffix.Failure == Downstream.Failure, Suffix.Output == Downstream.Input
|
||||
{
|
||||
let inner = Inner(downstream: subscriber, suffix: suffix)
|
||||
prefix.subscribe(inner)
|
||||
subscriber.receive(subscription: inner)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.Concatenate: Equatable where Prefix: Equatable, Suffix: Equatable {}
|
||||
|
||||
extension Publishers.Concatenate {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Suffix.Output, Downstream.Failure == Suffix.Failure
|
||||
{
|
||||
typealias Input = Suffix.Output
|
||||
|
||||
typealias Failure = Suffix.Failure
|
||||
|
||||
private let downstream: Downstream
|
||||
|
||||
private let suffix: Suffix
|
||||
|
||||
private var prefixFinished = false
|
||||
|
||||
private var demand = Subscribers.Demand.none
|
||||
|
||||
private var upstream: Subscription?
|
||||
|
||||
private var expectedSubscriptions = 2
|
||||
|
||||
private let lock = UnfairLock.allocate()
|
||||
|
||||
private let downstreamLock = UnfairRecursiveLock.allocate()
|
||||
|
||||
fileprivate init(downstream: Downstream, suffix: Suffix) {
|
||||
self.downstream = downstream
|
||||
self.suffix = suffix
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
downstreamLock.deallocate()
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard upstream == nil, expectedSubscriptions > 0 else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
upstream = subscription
|
||||
expectedSubscriptions -= 1
|
||||
let demand = self.demand
|
||||
lock.unlock()
|
||||
if demand > 0 {
|
||||
subscription.request(demand)
|
||||
}
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
demand -= 1
|
||||
lock.unlock()
|
||||
downstreamLock.lock()
|
||||
let newDemand = downstream.receive(input)
|
||||
downstreamLock.unlock()
|
||||
lock.lock()
|
||||
demand += newDemand
|
||||
lock.unlock()
|
||||
return newDemand
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
// Reading prefixFinished should be locked. Combine doesn't lock here.
|
||||
if prefixFinished {
|
||||
downstreamLock.lock()
|
||||
downstream.receive(completion: completion)
|
||||
downstreamLock.unlock()
|
||||
return
|
||||
}
|
||||
|
||||
guard case .finished = completion else {
|
||||
downstreamLock.lock()
|
||||
downstream.receive(completion: completion)
|
||||
downstreamLock.unlock()
|
||||
return
|
||||
}
|
||||
|
||||
prefixFinished = true // Should be locked as well?
|
||||
lock.lock()
|
||||
upstream = nil
|
||||
lock.unlock()
|
||||
suffix.subscribe(self)
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
lock.lock()
|
||||
self.demand += demand
|
||||
guard let subscription = upstream else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
lock.unlock()
|
||||
subscription.request(demand)
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
guard let subscription = upstream else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
upstream = nil
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
}
|
||||
|
||||
var description: String { return "Concatenate" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
let children: [Mirror.Child] = [
|
||||
("downstream", downstream),
|
||||
("upstreamSubscription", upstream as Any),
|
||||
("suffix", suffix),
|
||||
("demand", demand)
|
||||
]
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
//
|
||||
// Publishers.Delay.swift
|
||||
// OpenCombine
|
||||
//
|
||||
// Created by Евгений Богомолов on 07/09/2019.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Delays delivery of all output to the downstream receiver by a specified amount
|
||||
/// of time on a particular scheduler.
|
||||
///
|
||||
/// The delay affects the delivery of elements and completion, but not of the original
|
||||
/// subscription.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - interval: The amount of time to delay.
|
||||
/// - tolerance: The allowed tolerance in firing delayed events.
|
||||
/// - scheduler: The scheduler to deliver the delayed events.
|
||||
/// - Returns: A publisher that delays delivery of elements and completion to
|
||||
/// the downstream receiver.
|
||||
public func delay<Context: Scheduler>(
|
||||
for interval: Context.SchedulerTimeType.Stride,
|
||||
tolerance: Context.SchedulerTimeType.Stride? = nil,
|
||||
scheduler: Context,
|
||||
options: Context.SchedulerOptions? = nil
|
||||
) -> Publishers.Delay<Self, Context> {
|
||||
return .init(upstream: self,
|
||||
interval: interval,
|
||||
tolerance: tolerance ?? scheduler.minimumTolerance,
|
||||
scheduler: scheduler,
|
||||
options: options)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that delays delivery of elements and completion
|
||||
/// to the downstream receiver.
|
||||
public struct Delay<Upstream: Publisher, Context: Scheduler>: Publisher {
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher that this publisher receives elements from.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The amount of time to delay.
|
||||
public let interval: Context.SchedulerTimeType.Stride
|
||||
|
||||
/// The allowed tolerance in firing delayed events.
|
||||
public let tolerance: Context.SchedulerTimeType.Stride
|
||||
|
||||
/// The scheduler to deliver the delayed events.
|
||||
public let scheduler: Context
|
||||
|
||||
public let options: Context.SchedulerOptions?
|
||||
|
||||
public init(upstream: Upstream,
|
||||
interval: Context.SchedulerTimeType.Stride,
|
||||
tolerance: Context.SchedulerTimeType.Stride,
|
||||
scheduler: Context,
|
||||
options: Context.SchedulerOptions? = nil)
|
||||
{
|
||||
self.upstream = upstream
|
||||
self.interval = interval
|
||||
self.tolerance = tolerance
|
||||
self.scheduler = scheduler
|
||||
self.options = options
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Failure == Downstream.Failure,
|
||||
Upstream.Output == Downstream.Input
|
||||
{
|
||||
upstream.subscribe(Inner(self, downstream: subscriber))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.Delay {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
Subscription
|
||||
where Downstream.Input == Upstream.Output, Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
// NOTE: This class has been audited for thread safety
|
||||
|
||||
typealias Input = Upstream.Output
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
fileprivate typealias Delay = Publishers.Delay<Upstream, Context>
|
||||
|
||||
private enum State {
|
||||
case ready(Delay, Downstream)
|
||||
case subscribed(Delay, Downstream, Subscription)
|
||||
case terminal
|
||||
}
|
||||
|
||||
private let lock = UnfairLock.allocate()
|
||||
private var state: State
|
||||
private let downstreamLock = UnfairRecursiveLock.allocate()
|
||||
|
||||
fileprivate init(_ publisher: Delay, downstream: Downstream) {
|
||||
state = .ready(publisher, downstream)
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
downstreamLock.deallocate()
|
||||
}
|
||||
|
||||
private func schedule(_ delay: Delay, work: @escaping () -> Void) {
|
||||
delay
|
||||
.scheduler
|
||||
.schedule(after: delay.scheduler.now.advanced(by: delay.interval),
|
||||
tolerance: delay.tolerance,
|
||||
options: delay.options,
|
||||
work)
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard case let .ready(delay, downstream) = state else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
state = .subscribed(delay, downstream, subscription)
|
||||
lock.unlock()
|
||||
downstreamLock.lock()
|
||||
downstream.receive(subscription: self)
|
||||
downstreamLock.unlock()
|
||||
}
|
||||
|
||||
func receive(_ input: Upstream.Output) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
guard case let .subscribed(delay, downstream, _) = state else {
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
lock.unlock()
|
||||
schedule(delay) {
|
||||
self.scheduledReceive(input, downstream: downstream)
|
||||
}
|
||||
return .none
|
||||
}
|
||||
|
||||
private func scheduledReceive(_ input: Upstream.Output, downstream: Downstream) {
|
||||
downstreamLock.lock()
|
||||
let newDemand = downstream.receive(input)
|
||||
downstreamLock.unlock()
|
||||
guard newDemand > 0 else {
|
||||
return
|
||||
}
|
||||
lock.lock()
|
||||
guard case let .subscribed(_, _, subscription) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
lock.unlock()
|
||||
subscription.request(newDemand)
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
lock.lock()
|
||||
guard case let .subscribed(delay, downstream, _) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
state = .terminal
|
||||
lock.unlock()
|
||||
schedule(delay) {
|
||||
self.scheduledReceive(completion: completion, downstream: downstream)
|
||||
}
|
||||
}
|
||||
|
||||
private func scheduledReceive(completion: Subscribers.Completion<Failure>,
|
||||
downstream: Downstream) {
|
||||
downstreamLock.lock()
|
||||
downstream.receive(completion: completion)
|
||||
downstreamLock.unlock()
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
lock.lock()
|
||||
guard case let .subscribed(_, _, subscription) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
lock.unlock()
|
||||
subscription.request(demand)
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
guard case let .subscribed(_, _, subscription) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
state = .terminal
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,6 @@
|
||||
// Created by Sven Weidauer on 03.10.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
extension Publisher {
|
||||
/// Omits the specified number of elements before republishing subsequent elements.
|
||||
///
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
//
|
||||
// Publishers.DropUntilOutput.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 24.12.2019.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Ignores elements from the upstream publisher until it receives an element from
|
||||
/// a second publisher.
|
||||
///
|
||||
/// This publisher requests a single value from the upstream publisher, and it ignores
|
||||
/// (drops) all elements from that publisher until the upstream publisher produces
|
||||
/// a value. After the `other` publisher produces an element, this publisher cancels
|
||||
/// its subscription to the `other` publisher, and allows events from the `upstream`
|
||||
/// publisher to pass through.
|
||||
/// After this publisher receives a subscription from the upstream publisher, it
|
||||
/// passes through backpressure requests from downstream to the upstream publisher.
|
||||
/// If the upstream publisher acts on those requests before the other publisher
|
||||
/// produces an item, this publisher drops the elements it receives from the upstream
|
||||
/// publisher.
|
||||
///
|
||||
/// - Parameter publisher: A publisher to monitor for its first emitted element.
|
||||
/// - Returns: A publisher that drops elements from the upstream publisher until the
|
||||
/// `other` publisher produces a value.
|
||||
public func drop<Other: Publisher>(
|
||||
untilOutputFrom publisher: Other
|
||||
) -> Publishers.DropUntilOutput<Self, Other> where Failure == Other.Failure {
|
||||
return .init(upstream: self, other: publisher)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that ignores elements from the upstream publisher until it receives
|
||||
/// an element from second publisher.
|
||||
public struct DropUntilOutput<Upstream: Publisher, Other: Publisher>: Publisher
|
||||
where Upstream.Failure == Other.Failure
|
||||
{
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher that this publisher receives elements from.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// A publisher to monitor for its first emitted element.
|
||||
public let other: Other
|
||||
|
||||
/// Creates a publisher that ignores elements from the upstream publisher until
|
||||
/// it receives an element from another publisher.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - upstream: A publisher to drop elements from while waiting for another
|
||||
/// publisher to emit elements.
|
||||
/// - other: A publisher to monitor for its first emitted element.
|
||||
public init(upstream: Upstream, other: Other) {
|
||||
self.upstream = upstream
|
||||
self.other = other
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Output == Downstream.Input,
|
||||
Other.Failure == Downstream.Failure
|
||||
{
|
||||
let inner = Inner(downstream: subscriber)
|
||||
other.subscribe(Inner.OtherSubscriber(inner: inner))
|
||||
upstream.subscribe(inner)
|
||||
subscriber.receive(subscription: inner)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.DropUntilOutput: Equatable
|
||||
where Upstream: Equatable, Other: Equatable {}
|
||||
|
||||
extension Publishers.DropUntilOutput {
|
||||
fileprivate final class Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Upstream.Output, Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
private let downstream: Downstream
|
||||
|
||||
private var triggered = false
|
||||
|
||||
private let lock = UnfairLock.allocate()
|
||||
|
||||
private let downstreamLock = UnfairRecursiveLock.allocate()
|
||||
|
||||
private var upstreamSubscription: Subscription?
|
||||
|
||||
private var pendingDemand = Subscribers.Demand.none
|
||||
|
||||
private var otherSubscription: Subscription?
|
||||
|
||||
private var otherFinished = false
|
||||
|
||||
private var cancelled = false
|
||||
|
||||
init(downstream: Downstream) {
|
||||
self.downstream = downstream
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
downstreamLock.deallocate()
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard upstreamSubscription == nil && !cancelled else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
upstreamSubscription = subscription
|
||||
if pendingDemand > 0 {
|
||||
lock.unlock()
|
||||
subscription.request(pendingDemand)
|
||||
} else {
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
if !triggered || cancelled {
|
||||
pendingDemand -= 1
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
lock.unlock()
|
||||
downstreamLock.lock()
|
||||
let newDemand = downstream.receive(input)
|
||||
downstreamLock.unlock()
|
||||
return newDemand
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
lock.lock()
|
||||
if cancelled {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
cancelled = true
|
||||
lock.unlock()
|
||||
downstreamLock.lock()
|
||||
downstream.receive(completion: completion)
|
||||
downstreamLock.unlock()
|
||||
}
|
||||
|
||||
private func receiveOther(subscription: Subscription) {
|
||||
// Combine doesn't lock here
|
||||
guard otherSubscription == nil else {
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
otherSubscription = subscription
|
||||
subscription.request(.max(1))
|
||||
}
|
||||
|
||||
private func receiveOther(_ input: Other.Output) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
triggered = true
|
||||
otherSubscription = nil
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
|
||||
private func receiveOther(completion: Subscribers.Completion<Other.Failure>) {
|
||||
lock.lock()
|
||||
if triggered {
|
||||
otherSubscription = nil
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
|
||||
otherFinished = true
|
||||
if let upstreamSubscription = self.upstreamSubscription {
|
||||
self.upstreamSubscription = nil
|
||||
lock.unlock()
|
||||
upstreamSubscription.cancel()
|
||||
} else {
|
||||
lock.unlock()
|
||||
}
|
||||
downstreamLock.lock()
|
||||
downstream.receive(completion: completion)
|
||||
downstreamLock.unlock()
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
lock.lock()
|
||||
pendingDemand += demand
|
||||
if let subscription = upstreamSubscription {
|
||||
lock.unlock()
|
||||
subscription.request(demand)
|
||||
} else {
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
let upstreamSubscription = self.upstreamSubscription
|
||||
let otherSubscription = self.otherSubscription
|
||||
self.upstreamSubscription = nil
|
||||
self.otherSubscription = nil
|
||||
cancelled = true
|
||||
lock.unlock()
|
||||
|
||||
upstreamSubscription?.cancel()
|
||||
otherSubscription?.cancel()
|
||||
}
|
||||
|
||||
var description: String { return "DropUntilOutput" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
return Mirror(self, children: EmptyCollection())
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.DropUntilOutput.Inner {
|
||||
fileprivate struct OtherSubscriber
|
||||
: Subscriber,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
{
|
||||
let inner: Publishers.DropUntilOutput<Upstream, Other>.Inner<Downstream>
|
||||
|
||||
var combineIdentifier: CombineIdentifier {
|
||||
return inner.combineIdentifier
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
inner.receiveOther(subscription: subscription)
|
||||
}
|
||||
|
||||
func receive(_ input: Other.Output) -> Subscribers.Demand {
|
||||
return inner.receiveOther(input)
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Other.Failure>) {
|
||||
inner.receiveOther(completion: completion)
|
||||
}
|
||||
|
||||
var description: String { return "DropUntilOutput" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
return Mirror(self, children: EmptyCollection())
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,6 @@
|
||||
// Created by Sergej Jaskiewicz on 16.06.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Omits elements from the upstream publisher until a given closure returns false,
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
// Created by Eric Patey on 16.08.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
extension Publisher {
|
||||
/// Transforms all elements from an upstream publisher into a new or existing
|
||||
/// publisher.
|
||||
@@ -90,10 +88,9 @@ extension Publishers.FlatMap {
|
||||
/// by the `downstreamLock`.
|
||||
private let lock = UnfairLock.allocate()
|
||||
|
||||
// Must be recursive lock. Probably a bug in Combine.
|
||||
/// All the calls to the downstream subscriber should be made with this lock
|
||||
/// acquired.
|
||||
private let downstreamLock = UnfairLock.allocate()
|
||||
private let downstreamLock = UnfairRecursiveLock.allocate()
|
||||
|
||||
private let downstream: Downstream
|
||||
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
//
|
||||
// Publishers.HandleEvents.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 03.12.2019.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Performs the specified closures when publisher events occur.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - receiveSubscription: A closure that executes when the publisher receives
|
||||
/// the subscription from the upstream publisher. Defaults to `nil`.
|
||||
/// - receiveOutput: A closure that executes when the publisher receives a value
|
||||
/// from the upstream publisher. Defaults to `nil`.
|
||||
/// - receiveCompletion: A closure that executes when the publisher receives
|
||||
/// the completion from the upstream publisher. Defaults to `nil`.
|
||||
/// - receiveCancel: A closure that executes when the downstream receiver cancels
|
||||
/// publishing. Defaults to `nil`.
|
||||
/// - receiveRequest: A closure that executes when the publisher receives a request
|
||||
/// for more elements. Defaults to `nil`.
|
||||
/// - Returns: A publisher that performs the specified closures when publisher events
|
||||
/// occur.
|
||||
public func handleEvents(
|
||||
receiveSubscription: ((Subscription) -> Void)? = nil,
|
||||
receiveOutput: ((Output) -> Void)? = nil,
|
||||
receiveCompletion: ((Subscribers.Completion<Failure>) -> Void)? = nil,
|
||||
receiveCancel: (() -> Void)? = nil,
|
||||
receiveRequest: ((Subscribers.Demand) -> Void)? = nil
|
||||
) -> Publishers.HandleEvents<Self> {
|
||||
return .init(upstream: self,
|
||||
receiveSubscription: receiveSubscription,
|
||||
receiveOutput: receiveOutput,
|
||||
receiveCompletion: receiveCompletion,
|
||||
receiveCancel: receiveCancel,
|
||||
receiveRequest: receiveRequest)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that performs the specified closures when publisher events occur.
|
||||
public struct HandleEvents<Upstream: Publisher>: Publisher {
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// A closure that executes when the publisher receives the subscription from
|
||||
/// the upstream publisher.
|
||||
public var receiveSubscription: ((Subscription) -> Void)?
|
||||
|
||||
/// A closure that executes when the publisher receives a value from the upstream
|
||||
/// publisher.
|
||||
public var receiveOutput: ((Upstream.Output) -> Void)?
|
||||
|
||||
/// A closure that executes when the publisher receives the completion from
|
||||
/// the upstream publisher.
|
||||
public var receiveCompletion:
|
||||
((Subscribers.Completion<Upstream.Failure>) -> Void)?
|
||||
|
||||
/// A closure that executes when the downstream receiver cancels publishing.
|
||||
public var receiveCancel: (() -> Void)?
|
||||
|
||||
/// A closure that executes when the publisher receives a request for more
|
||||
/// elements.
|
||||
public var receiveRequest: ((Subscribers.Demand) -> Void)?
|
||||
|
||||
public init(
|
||||
upstream: Upstream,
|
||||
receiveSubscription: ((Subscription) -> Void)? = nil,
|
||||
receiveOutput: ((Output) -> Void)? = nil,
|
||||
receiveCompletion: ((Subscribers.Completion<Failure>) -> Void)? = nil,
|
||||
receiveCancel: (() -> Void)? = nil,
|
||||
receiveRequest: ((Subscribers.Demand) -> Void)?
|
||||
) {
|
||||
self.upstream = upstream
|
||||
self.receiveSubscription = receiveSubscription
|
||||
self.receiveOutput = receiveOutput
|
||||
self.receiveCompletion = receiveCompletion
|
||||
self.receiveCancel = receiveCancel
|
||||
self.receiveRequest = receiveRequest
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Failure == Downstream.Failure,
|
||||
Upstream.Output == Downstream.Input
|
||||
{
|
||||
let inner = Inner(self, downstream: subscriber)
|
||||
subscriber.receive(subscription: inner)
|
||||
upstream.subscribe(inner)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.HandleEvents {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Upstream.Output, Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
private var status = SubscriptionStatus.awaitingSubscription
|
||||
private var pendingDemand = Subscribers.Demand.none
|
||||
private let lock = UnfairLock.allocate()
|
||||
private var events: Publishers.HandleEvents<Upstream>?
|
||||
private let downstream: Downstream
|
||||
|
||||
init(_ events: Publishers.HandleEvents<Upstream>, downstream: Downstream) {
|
||||
self.events = events
|
||||
self.downstream = downstream
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
events?.receiveSubscription?(subscription)
|
||||
lock.lock()
|
||||
guard case .awaitingSubscription = status else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
status = .subscribed(subscription)
|
||||
let pendingDemand = self.pendingDemand
|
||||
self.pendingDemand = .none
|
||||
lock.unlock()
|
||||
if pendingDemand > 0 {
|
||||
subscription.request(pendingDemand)
|
||||
}
|
||||
}
|
||||
|
||||
func receive(_ input: Upstream.Output) -> Subscribers.Demand {
|
||||
events?.receiveOutput?(input)
|
||||
let newDemand = downstream.receive(input)
|
||||
if newDemand > 0 {
|
||||
events?.receiveRequest?(newDemand)
|
||||
}
|
||||
return newDemand
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Upstream.Failure>) {
|
||||
events?.receiveCompletion?(completion)
|
||||
lock.lock()
|
||||
events = nil
|
||||
status = .terminal
|
||||
lock.unlock()
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
events?.receiveRequest?(demand)
|
||||
lock.lock()
|
||||
if case let .subscribed(subscription) = status {
|
||||
lock.unlock()
|
||||
subscription.request(demand)
|
||||
return
|
||||
}
|
||||
pendingDemand += demand
|
||||
lock.unlock()
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
events?.receiveCancel?()
|
||||
lock.lock()
|
||||
guard case let .subscribed(subscription) = status else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
events = nil
|
||||
status = .terminal
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
}
|
||||
|
||||
var description: String { return "HandleEvents" }
|
||||
|
||||
var customMirror: Mirror { return Mirror(self, children: EmptyCollection()) }
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,6 @@
|
||||
// Created by Eric Patey on 16.08.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Ingores all upstream elements, but passes along a completion
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
// Created by Anton Nazarov on 25.06.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Transforms all elements from the upstream publisher with a provided closure.
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
//
|
||||
// Publishers.MeasureInterval.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 03.12.2019.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Measures and emits the time interval between events received from an upstream
|
||||
/// publisher.
|
||||
///
|
||||
/// The output type of the returned scheduler is the time interval of the provided
|
||||
/// scheduler.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - scheduler: The scheduler on which to deliver elements.
|
||||
/// - options: Options that customize the delivery of elements.
|
||||
/// - Returns: A publisher that emits elements representing the time interval between
|
||||
/// the elements it receives.
|
||||
public func measureInterval<Context: Scheduler>(
|
||||
using scheduler: Context,
|
||||
options: Context.SchedulerOptions? = nil
|
||||
) -> Publishers.MeasureInterval<Self, Context> {
|
||||
return .init(upstream: self, scheduler: scheduler)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that measures and emits the time interval between events received from
|
||||
/// an upstream publisher.
|
||||
public struct MeasureInterval<Upstream: Publisher, Context: Scheduler>: Publisher {
|
||||
|
||||
public typealias Output = Context.SchedulerTimeType.Stride
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The scheduler on which to deliver elements.
|
||||
public let scheduler: Context
|
||||
|
||||
public init(upstream: Upstream, scheduler: Context) {
|
||||
self.upstream = upstream
|
||||
self.scheduler = scheduler
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Failure == Downstream.Failure,
|
||||
Downstream.Input == Context.SchedulerTimeType.Stride
|
||||
{
|
||||
upstream.subscribe(Inner(self, downstream: subscriber))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.MeasureInterval {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Context.SchedulerTimeType.Stride,
|
||||
Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
// NOTE: This class has been audited for thread safety
|
||||
|
||||
typealias Input = Upstream.Output
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
typealias MeasureInterval = Publishers.MeasureInterval<Upstream, Context>
|
||||
|
||||
private enum State {
|
||||
case ready(MeasureInterval, Downstream)
|
||||
case subscribed(MeasureInterval, Downstream, Subscription)
|
||||
case terminal
|
||||
}
|
||||
|
||||
private let lock = UnfairLock.allocate()
|
||||
|
||||
private var state: State
|
||||
|
||||
private var last: Context.SchedulerTimeType?
|
||||
|
||||
init(_ measureInterval: MeasureInterval, downstream: Downstream) {
|
||||
state = .ready(measureInterval, downstream)
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard case let .ready(measureInterval, downstream) = state else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
state = .subscribed(measureInterval, downstream, subscription)
|
||||
last = measureInterval.scheduler.now
|
||||
lock.unlock()
|
||||
downstream.receive(subscription: self)
|
||||
}
|
||||
|
||||
func receive(_: Input) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
guard case let .subscribed(measureInterval, downstream, subscription) = state,
|
||||
let previousTime = last else {
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
let now = measureInterval.scheduler.now
|
||||
last = now
|
||||
lock.unlock()
|
||||
let newDemand = downstream.receive(previousTime.distance(to: now))
|
||||
if newDemand > 0 {
|
||||
subscription.request(newDemand)
|
||||
}
|
||||
return .none
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
lock.lock()
|
||||
guard case let .subscribed(_, downstream, _) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
state = .terminal
|
||||
last = nil
|
||||
lock.unlock()
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
lock.lock()
|
||||
guard case let .subscribed(_, _, subscription) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
lock.unlock()
|
||||
subscription.request(demand)
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
guard case let .subscribed(_, _, subscription) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
state = .terminal
|
||||
last = nil
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
}
|
||||
|
||||
var description: String { return "MeasureInterval" }
|
||||
|
||||
var customMirror: Mirror { return Mirror(self, children: EmptyCollection()) }
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
${template_header}
|
||||
//
|
||||
// Publishers.Merge.swift.gyb
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 04/10/2019.
|
||||
//
|
||||
%{
|
||||
from gyb_opencombine_support import (
|
||||
suffix_variadic,
|
||||
list_with_suffix_variadic,
|
||||
indent
|
||||
)
|
||||
|
||||
import string
|
||||
|
||||
instantiations = [(2, 'two', 'A second'),
|
||||
(3, 'three', 'A third'),
|
||||
(4, 'four', 'A fourth'),
|
||||
(5, 'five', 'A fifth'),
|
||||
(6, 'six', 'A sixth'),
|
||||
(7, 'seven', 'A seventh'),
|
||||
(8, 'eight', 'An eighth')]
|
||||
|
||||
def make_publisher_name(arity):
|
||||
return suffix_variadic('Merge', arity, arity - 1)
|
||||
|
||||
def make_upstream_types(arity, start=0):
|
||||
return [str(c) for c in string.ascii_uppercase[start:arity]]
|
||||
|
||||
def make_upstream_types_reversed(arity):
|
||||
return [str(c) for c in reversed(string.ascii_uppercase)][:arity]
|
||||
|
||||
def make_upstream_generic_constraints(upstream_types, first_is_self=False):
|
||||
|
||||
format_string = '{0}Failure == {1}.Failure, {0}Output == {1}.Output'
|
||||
|
||||
def format(i):
|
||||
return format_string.format(upstream_types[i] + '.',
|
||||
upstream_types[i + 1])
|
||||
|
||||
result = [format(i) for i in range(len(upstream_types) - 1)]
|
||||
|
||||
if first_is_self:
|
||||
result.insert(0, format_string.format('', upstream_types[0]))
|
||||
|
||||
return result
|
||||
|
||||
def declare_merge_method(arg_count, arity, indent_spaces_count):
|
||||
assert(arg_count <= arity - 1)
|
||||
is_specialization = arg_count < arity - 1
|
||||
|
||||
declaration_format = """\
|
||||
public func merge<
|
||||
{}
|
||||
>(with {}) -> Publishers.{}<{}>\
|
||||
"""
|
||||
|
||||
where_clause_format = '\n where {}'
|
||||
|
||||
if arg_count == 1:
|
||||
upstream_types = ['P']
|
||||
elif is_specialization:
|
||||
upstream_types = make_upstream_types_reversed(arg_count)
|
||||
else:
|
||||
upstream_types = make_upstream_types(arg_count + 1, 1)
|
||||
|
||||
method_generic_params = \
|
||||
[upstream_type + ': Publisher' for upstream_type in upstream_types]
|
||||
|
||||
cs_method_generic_params = \
|
||||
(',\n ').join(method_generic_params)
|
||||
|
||||
method_args = ['other: P'] \
|
||||
if arg_count == 1 else ['{}: {}'.format(upstream_type.lower(), upstream_type) \
|
||||
for upstream_type in upstream_types]
|
||||
|
||||
cs_method_args = ',\n _ '.join(method_args)
|
||||
|
||||
publisher_name = make_publisher_name(arity)
|
||||
|
||||
self_generic_params = make_upstream_types(arity - arg_count) \
|
||||
if is_specialization else ['Self']
|
||||
|
||||
publisher_generic_params = self_generic_params + upstream_types
|
||||
|
||||
cs_publisher_generic_params = ', '.join(publisher_generic_params)
|
||||
|
||||
generic_constraints = make_upstream_generic_constraints(upstream_types, True)
|
||||
|
||||
cs_generic_constraints = \
|
||||
',\n '.join(generic_constraints)
|
||||
|
||||
declaration = declaration_format.format(cs_method_generic_params,
|
||||
cs_method_args,
|
||||
publisher_name,
|
||||
cs_publisher_generic_params)
|
||||
|
||||
if not is_specialization:
|
||||
declaration += where_clause_format.format(cs_generic_constraints)
|
||||
|
||||
return indent(declaration, indent_spaces_count)
|
||||
}%
|
||||
|
||||
// swiftlint:disable generic_type_name
|
||||
// swiftlint:disable vertical_parameter_alignment
|
||||
|
||||
// MARK: - Merge methods on Publisher
|
||||
|
||||
extension Publisher {
|
||||
|
||||
% for arity, _, _ in instantiations:
|
||||
%
|
||||
% doc_cardinal = 'another publisher' \
|
||||
% if arity == 2 else (instantiations[arity - 2][1] + ' other publishers')
|
||||
% argument_names = ['other'] \
|
||||
% if arity == 2 else [upstream_type.lower() \
|
||||
% for upstream_type in make_upstream_types(arity, 1)]
|
||||
/// Combines elements from this publisher with those from ${doc_cardinal},
|
||||
/// delivering an interleaved sequence of elements.
|
||||
///
|
||||
/// The merged publisher continues to emit elements until all upstream publishers
|
||||
/// finish. If an upstream publisher produces an error, the merged publisher fails
|
||||
/// with that error.
|
||||
///
|
||||
/// - Parameters:
|
||||
% for i in range(arity - 1):
|
||||
% param_doc = 'Another' if arity == 2 else instantiations[i][2]
|
||||
/// - ${argument_names[i]}: ${param_doc} publisher.
|
||||
% end
|
||||
/// - Returns: A publisher that emits an event when any upstream publisher emits
|
||||
/// an event.
|
||||
${declare_merge_method(arity - 1, arity, 4)}
|
||||
{
|
||||
return .init(self, ${', '.join(argument_names)})
|
||||
}
|
||||
% end
|
||||
}
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Combines elements from this publisher with those from another publisher of
|
||||
/// the same type, delivering an interleaved sequence of elements.
|
||||
///
|
||||
/// - Parameter other: Another publisher of this publisher's type.
|
||||
/// - Returns: A publisher that emits an event when either upstream publisher emits
|
||||
/// an event.
|
||||
public func merge(with other: Self) -> Publishers.MergeMany<Self> {
|
||||
return .init([self, other])
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Merge publishers
|
||||
|
||||
extension Publishers {
|
||||
% for arity, cardinal, _ in instantiations:
|
||||
%
|
||||
% publisher_name = make_publisher_name(arity)
|
||||
%
|
||||
% upstream_types = make_upstream_types(arity)
|
||||
%
|
||||
% upstream_generic_params = \
|
||||
% [upstream_type + ': Publisher' for upstream_type in upstream_types]
|
||||
%
|
||||
% cs_upstream_generic_params = \
|
||||
% (',\n' + (19 + len(publisher_name)) * ' ').join(upstream_generic_params)
|
||||
%
|
||||
% upstream_generic_constraints = \
|
||||
% make_upstream_generic_constraints(upstream_types)
|
||||
%
|
||||
% cs_upstream_generic_constraints = \
|
||||
% ',\n '.join(upstream_generic_constraints)
|
||||
%
|
||||
% init_args = ['_ {}: {}'.format(upstream_type.lower(), upstream_type) \
|
||||
% for upstream_type in upstream_types]
|
||||
% cs_init_args = ',\n '.join(init_args)
|
||||
%
|
||||
% self_fields = [upstream_type.lower() for upstream_type in upstream_types]
|
||||
|
||||
/// A publisher created by applying the merge function to ${cardinal} upstream
|
||||
/// publishers.
|
||||
public struct ${publisher_name}<${cs_upstream_generic_params}>: Publisher
|
||||
where ${cs_upstream_generic_constraints}
|
||||
{
|
||||
public typealias Output = ${upstream_types[0]}.Output
|
||||
|
||||
public typealias Failure = ${upstream_types[0]}.Failure
|
||||
% for upstream_type in upstream_types:
|
||||
|
||||
public let ${upstream_type.lower()}: ${upstream_type}
|
||||
% end
|
||||
|
||||
public init(
|
||||
${cs_init_args}
|
||||
) {
|
||||
% for self_field in self_fields:
|
||||
self.${self_field} = ${self_field}
|
||||
% end
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where ${upstream_types[0]}.Failure == Downstream.Failure,
|
||||
${upstream_types[0]}.Output == Downstream.Input
|
||||
{
|
||||
typealias Merged = _Merged<Output, Failure, Downstream>
|
||||
let merged = Merged(downstream: subscriber, count: ${arity})
|
||||
% for i in range(len(self_fields)):
|
||||
${self_fields[i]}.subscribe(Merged.Side(index: ${i}, merger: merged))
|
||||
% end
|
||||
subscriber.receive(subscription: merged)
|
||||
}
|
||||
% for i in range(len(instantiations) + 1 - arity):
|
||||
% argument_names = ['other'] \
|
||||
% if i == 0 else [upstream_type.lower() \
|
||||
% for upstream_type in make_upstream_types_reversed(i + 1)]
|
||||
%
|
||||
|
||||
${declare_merge_method(i + 1, arity + i + 1, 8)}
|
||||
{
|
||||
return .init(${', '.join(self_fields + argument_names)})
|
||||
}
|
||||
% end
|
||||
}
|
||||
% end
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
public struct MergeMany<Upstream: Publisher>: Publisher {
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
public let publishers: [Upstream]
|
||||
|
||||
public init(_ upstream: Upstream...) {
|
||||
self.publishers = upstream
|
||||
}
|
||||
|
||||
public init<UpstreamPublishers: Swift.Sequence>(_ upstream: UpstreamPublishers)
|
||||
where Upstream == UpstreamPublishers.Element
|
||||
{
|
||||
publishers = Array(upstream)
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Failure == Downstream.Failure,
|
||||
Upstream.Output == Downstream.Input
|
||||
{
|
||||
typealias Merged = _Merged<Output, Failure, Downstream>
|
||||
let merged = Merged(downstream: subscriber, count: publishers.count)
|
||||
for (i, upstream) in publishers.enumerated() {
|
||||
upstream.subscribe(Merged.Side(index: i, merger: merged))
|
||||
}
|
||||
subscriber.receive(subscription: merged)
|
||||
}
|
||||
|
||||
public func merge(with other: Upstream) -> Publishers.MergeMany<Upstream> {
|
||||
var newPublishers = publishers
|
||||
newPublishers.append(other)
|
||||
return .init(newPublishers)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Equatable conformances
|
||||
% for arity, cardinal, _ in instantiations:
|
||||
%
|
||||
% publisher_name = make_publisher_name(arity)
|
||||
%
|
||||
% upstream_types = make_upstream_types(arity)
|
||||
%
|
||||
% constraints = [upstream_type + ': Equatable' for upstream_type in upstream_types]
|
||||
% cs_constraints = ',\n'.join(constraints)
|
||||
% cs_constraints = indent(cs_constraints, 8)
|
||||
%
|
||||
|
||||
extension Publishers.${publisher_name}: Equatable
|
||||
where
|
||||
${cs_constraints} {}
|
||||
% end
|
||||
|
||||
extension Publishers.MergeMany: Equatable
|
||||
where
|
||||
Upstream: Equatable {}
|
||||
@@ -5,8 +5,6 @@
|
||||
// Created by Sergej Jaskiewicz on 14.06.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Applies a closure to create a subject that delivers elements to subscribers.
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
// Created by Sergej Jaskiewicz on 24.10.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Republishes elements up to the specified maximum count.
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
// Created by Sergej Jaskiewicz on 16.06.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Prints log messages for all publishing events.
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
//
|
||||
// Publishers.ReceiveOn.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 02.12.2019.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
/// Specifies the scheduler on which to receive elements from the publisher.
|
||||
///
|
||||
/// You use the `receive(on:options:)` operator to receive results on a specific
|
||||
/// scheduler, such as performing UI work on the main run loop.
|
||||
/// In contrast with `subscribe(on:options:)`, which affects upstream messages,
|
||||
/// `receive(on:options:)` changes the execution context of downstream messages.
|
||||
/// In the following example, requests to `jsonPublisher` are performed on
|
||||
/// `backgroundQueue`, but elements received from it are performed on `RunLoop.main`.
|
||||
///
|
||||
/// // Some publisher.
|
||||
/// let jsonPublisher = MyJSONLoaderPublisher()
|
||||
///
|
||||
/// // Some subscriber that updates the UI.
|
||||
/// let labelUpdater = MyLabelUpdateSubscriber()
|
||||
///
|
||||
/// jsonPublisher
|
||||
/// .subscribe(on: backgroundQueue)
|
||||
/// .receiveOn(on: RunLoop.main)
|
||||
/// .subscribe(labelUpdater)
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - scheduler: The scheduler the publisher is to use for element delivery.
|
||||
/// - options: Scheduler options that customize the element delivery.
|
||||
/// - Returns: A publisher that delivers elements using the specified scheduler.
|
||||
public func receive<Context: Scheduler>(
|
||||
on scheduler: Context,
|
||||
options: Context.SchedulerOptions? = nil
|
||||
) -> Publishers.ReceiveOn<Self, Context> {
|
||||
return .init(upstream: self, scheduler: scheduler, options: options)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that delivers elements to its downstream subscriber on a specific
|
||||
/// scheduler.
|
||||
public struct ReceiveOn<Upstream: Publisher, Context: Scheduler>: Publisher {
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The scheduler the publisher is to use for element delivery.
|
||||
public let scheduler: Context
|
||||
|
||||
/// Scheduler options that customize the delivery of elements.
|
||||
public let options: Context.SchedulerOptions?
|
||||
|
||||
public init(upstream: Upstream,
|
||||
scheduler: Context,
|
||||
options: Context.SchedulerOptions?) {
|
||||
self.upstream = upstream
|
||||
self.scheduler = scheduler
|
||||
self.options = options
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Failure == Downstream.Failure,
|
||||
Upstream.Output == Downstream.Input
|
||||
{
|
||||
upstream.subscribe(Inner(self, downstream: subscriber))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.ReceiveOn {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Upstream.Output, Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
typealias ReceiveOn = Publishers.ReceiveOn<Upstream, Context>
|
||||
|
||||
private enum State {
|
||||
case ready(ReceiveOn, Downstream)
|
||||
case subscribed(ReceiveOn, Downstream, Subscription)
|
||||
case terminal
|
||||
}
|
||||
|
||||
private let lock = UnfairLock.allocate()
|
||||
private var state: State
|
||||
private let downstreamLock = UnfairRecursiveLock.allocate()
|
||||
|
||||
init(_ receiveOn: ReceiveOn, downstream: Downstream) {
|
||||
state = .ready(receiveOn, downstream)
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
downstreamLock.deallocate()
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard case let .ready(receiveOn, downstream) = state else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
state = .subscribed(receiveOn, downstream, subscription)
|
||||
lock.unlock()
|
||||
downstreamLock.lock()
|
||||
downstream.receive(subscription: self)
|
||||
downstreamLock.unlock()
|
||||
}
|
||||
|
||||
func receive(_ input: Upstream.Output) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
guard case let .subscribed(receiveOn, downstream, _) = state else {
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
lock.unlock()
|
||||
receiveOn.scheduler.schedule(options: receiveOn.options) { [weak self] in
|
||||
self?.scheduledReceive(input, downstream: downstream)
|
||||
}
|
||||
return .none
|
||||
}
|
||||
|
||||
private func scheduledReceive(_ input: Upstream.Output, downstream: Downstream) {
|
||||
downstreamLock.lock()
|
||||
let newDemand = downstream.receive(input)
|
||||
downstreamLock.unlock()
|
||||
guard newDemand > 0 else {
|
||||
return
|
||||
}
|
||||
lock.lock()
|
||||
guard case let .subscribed(_, _, subscription) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
lock.unlock()
|
||||
subscription.request(newDemand)
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Upstream.Failure>) {
|
||||
lock.lock()
|
||||
guard case let .subscribed(receiveOn, downstream, _) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
state = .terminal
|
||||
lock.unlock()
|
||||
receiveOn.scheduler.schedule(options: receiveOn.options) { [weak self] in
|
||||
self?.scheduledReceive(completion: completion, downstream: downstream)
|
||||
}
|
||||
}
|
||||
|
||||
private func scheduledReceive(completion: Subscribers.Completion<Failure>,
|
||||
downstream: Downstream) {
|
||||
downstreamLock.lock()
|
||||
downstream.receive(completion: completion)
|
||||
downstreamLock.unlock()
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
lock.lock()
|
||||
guard case let .subscribed(_, _, subscription) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
lock.unlock()
|
||||
subscription.request(demand)
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
guard case let .subscribed(_, _, subscription) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
state = .terminal
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
}
|
||||
|
||||
var description: String { return "ReceiveOn" }
|
||||
|
||||
var customMirror: Mirror { return Mirror(self, children: EmptyCollection()) }
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
//
|
||||
// Publishers.ReplaceEmpty.swift
|
||||
// OpenCombine
|
||||
//
|
||||
// Created by Joe Spadafora on 12/10/19.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Replaces an empty stream with the provided element.
|
||||
///
|
||||
/// If the upstream publisher finishes without producing any elements,
|
||||
/// this publisher emits the provided element, then finishes normally.
|
||||
/// - Parameter output: An element to emit when the upstream publisher
|
||||
/// finishes without emitting any elements.
|
||||
/// - Returns: A publisher that replaces an empty stream with
|
||||
/// the provided output element.
|
||||
public func replaceEmpty(with output: Output) -> Publishers.ReplaceEmpty<Self> {
|
||||
return .init(upstream: self, output: output)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that replaces an empty stream with a provided element.
|
||||
public struct ReplaceEmpty<Upstream: Publisher>: Publisher {
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The element to deliver when the upstream publisher finishes
|
||||
/// without delivering any elements.
|
||||
public let output: Upstream.Output
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
public init(upstream: Upstream, output: Output) {
|
||||
self.upstream = upstream
|
||||
self.output = output
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Failure == Downstream.Failure,
|
||||
Upstream.Output == Downstream.Input
|
||||
{
|
||||
let inner = Inner(downstream: subscriber, output: output)
|
||||
upstream.subscribe(inner)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.ReplaceEmpty: Equatable
|
||||
where Upstream: Equatable, Upstream.Output: Equatable {}
|
||||
|
||||
extension Publishers.ReplaceEmpty {
|
||||
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Upstream.Failure == Downstream.Failure,
|
||||
Upstream.Output == Downstream.Input
|
||||
{
|
||||
|
||||
typealias Input = Upstream.Output
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
private let output: Output
|
||||
private let downstream: Downstream
|
||||
|
||||
private var receivedUpstream = false
|
||||
private var lock = UnfairLock.allocate()
|
||||
private var downstreamRequested = false
|
||||
private var finishedWithoutUpstream = false
|
||||
|
||||
private var status = SubscriptionStatus.awaitingSubscription
|
||||
|
||||
fileprivate init(downstream: Downstream, output: Output) {
|
||||
self.downstream = downstream
|
||||
self.output = output
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
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)
|
||||
subscription.request(.unlimited)
|
||||
}
|
||||
|
||||
func receive(_ input: Upstream.Output) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
guard case .subscribed = status else {
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
receivedUpstream = true
|
||||
lock.unlock()
|
||||
return downstream.receive(input)
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Upstream.Failure>) {
|
||||
lock.lock()
|
||||
guard case .subscribed = status else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
status = .terminal
|
||||
if receivedUpstream {
|
||||
lock.unlock()
|
||||
downstream.receive(completion: completion)
|
||||
return
|
||||
}
|
||||
switch completion {
|
||||
case .finished:
|
||||
if downstreamRequested {
|
||||
lock.unlock()
|
||||
_ = downstream.receive(output)
|
||||
downstream.receive(completion: completion)
|
||||
return
|
||||
}
|
||||
finishedWithoutUpstream = true
|
||||
lock.unlock()
|
||||
case .failure:
|
||||
lock.unlock()
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
demand.assertNonZero()
|
||||
lock.lock()
|
||||
downstreamRequested = true
|
||||
if finishedWithoutUpstream {
|
||||
lock.unlock()
|
||||
_ = downstream.receive(output)
|
||||
downstream.receive(completion: .finished)
|
||||
return
|
||||
}
|
||||
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 "ReplaceEmpty" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
return Mirror(self, children: EmptyCollection())
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,6 @@
|
||||
// Created by Bogdan Vlad on 8/29/19.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
extension Publisher {
|
||||
/// Replaces any errors in the stream with the provided element.
|
||||
///
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
// Created by Eric Patey on 26.08.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Transforms elements from the upstream publisher by providing the current element
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
// Created by Sergej Jaskiewicz on 19.06.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that publishes a given sequence of elements.
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
//
|
||||
// Publishers.SubscribeOn.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 02.12.2019.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Specifies the scheduler on which to perform subscribe, cancel, and request
|
||||
/// operations.
|
||||
///
|
||||
/// In contrast with `receive(on:options:)`, which affects downstream messages,
|
||||
/// `subscribe(on:)` changes the execution context of upstream messages.
|
||||
/// In the following example, requests to `jsonPublisher` are performed on
|
||||
/// `backgroundQueue`, but elements received from it are performed on `RunLoop.main`.
|
||||
///
|
||||
/// let ioPerformingPublisher == // Some publisher.
|
||||
/// let uiUpdatingSubscriber == // Some subscriber that updates the UI.
|
||||
///
|
||||
/// ioPerformingPublisher
|
||||
/// .subscribe(on: backgroundQueue)
|
||||
/// .receiveOn(on: RunLoop.main)
|
||||
/// .subscribe(uiUpdatingSubscriber)
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - scheduler: The scheduler on which to receive upstream messages.
|
||||
/// - options: Options that customize the delivery of elements.
|
||||
/// - Returns: A publisher which performs upstream operations on the specified
|
||||
/// scheduler.
|
||||
public func subscribe<Context: Scheduler>(
|
||||
on scheduler: Context,
|
||||
options: Context.SchedulerOptions? = nil
|
||||
) -> Publishers.SubscribeOn<Self, Context> {
|
||||
return .init(upstream: self, scheduler: scheduler, options: options)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that receives elements from an upstream publisher on a specific
|
||||
/// scheduler.
|
||||
public struct SubscribeOn<Upstream: Publisher, Context: Scheduler>: Publisher {
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The scheduler the publisher should use to receive elements.
|
||||
public let scheduler: Context
|
||||
|
||||
/// Scheduler options that customize the delivery of elements.
|
||||
public let options: Context.SchedulerOptions?
|
||||
|
||||
public init(upstream: Upstream,
|
||||
scheduler: Context,
|
||||
options: Context.SchedulerOptions?) {
|
||||
self.upstream = upstream
|
||||
self.scheduler = scheduler
|
||||
self.options = options
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Failure == Downstream.Failure,
|
||||
Upstream.Output == Downstream.Input
|
||||
{
|
||||
scheduler.schedule(options: options) {
|
||||
self.upstream.subscribe(Inner(self, downstream: subscriber))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.SubscribeOn {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Upstream.Output, Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
typealias SubscribeOn = Publishers.SubscribeOn<Upstream, Context>
|
||||
|
||||
private enum State {
|
||||
case ready(SubscribeOn, Downstream)
|
||||
case subscribed(SubscribeOn, Downstream, Subscription)
|
||||
case terminal
|
||||
}
|
||||
|
||||
private let lock = UnfairLock.allocate()
|
||||
private var state: State
|
||||
private let upstreamLock = UnfairLock.allocate()
|
||||
|
||||
init(_ subscribeOn: SubscribeOn, downstream: Downstream) {
|
||||
state = .ready(subscribeOn, downstream)
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
upstreamLock.deallocate()
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard case let .ready(subscribeOn, downstream) = state else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
state = .subscribed(subscribeOn, downstream, subscription)
|
||||
lock.unlock()
|
||||
downstream.receive(subscription: self)
|
||||
}
|
||||
|
||||
func receive(_ input: Upstream.Output) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
guard case let .subscribed(_, downstream, _) = state else {
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
lock.unlock()
|
||||
return downstream.receive(input)
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Upstream.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) {
|
||||
lock.lock()
|
||||
guard case let .subscribed(subscribeOn, _, subscription) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
lock.unlock()
|
||||
subscribeOn.scheduler.schedule(options: subscribeOn.options) { [weak self] in
|
||||
self?.scheduledRequest(demand, subscription: subscription)
|
||||
}
|
||||
}
|
||||
|
||||
private func scheduledRequest(_ demand: Subscribers.Demand,
|
||||
subscription: Subscription) {
|
||||
upstreamLock.lock()
|
||||
subscription.request(demand)
|
||||
upstreamLock.unlock()
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
guard case let .subscribed(subscribeOn, _, subscription) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
state = .terminal
|
||||
lock.unlock()
|
||||
subscribeOn.scheduler.schedule(options: subscribeOn.options) { [weak self] in
|
||||
self?.scheduledCancel(subscription)
|
||||
}
|
||||
}
|
||||
|
||||
private func scheduledCancel(_ subscription: Subscription) {
|
||||
upstreamLock.lock()
|
||||
subscription.cancel()
|
||||
upstreamLock.unlock()
|
||||
}
|
||||
|
||||
var description: String { return "SubscribeOn" }
|
||||
|
||||
var customMirror: Mirror { return Mirror(self, children: EmptyCollection()) }
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
//
|
||||
// Publishers._Merged.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 03.12.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
extension Publishers {
|
||||
// swiftlint:disable:next type_name
|
||||
internal final class _Merged<Input, Failure, Downstream: Subscriber>
|
||||
: Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Input, Downstream.Failure == Failure
|
||||
{
|
||||
private let downstream: Downstream
|
||||
private var demand = Subscribers.Demand.none // 0x78
|
||||
private var terminated = false // 0x80
|
||||
private let count: Int // 0x88
|
||||
private var upstreamFinished = 0 // 0x90
|
||||
private var finished = false // 0x98
|
||||
|
||||
// TODO: The size of these arrays always stays the same.
|
||||
// Maybe we can leverage ManagedBuffer/ManagedBufferPointer here
|
||||
// to avoid additional allocations.
|
||||
private var subscriptions: [Subscription?] // 0xA0
|
||||
private var buffers: [Input?] // 0xA8
|
||||
|
||||
private let lock = UnfairLock.allocate() // 0xB0
|
||||
private let downstreamLock = UnfairLock.allocate() // 0xB8
|
||||
private var recursive = false // 0xC0
|
||||
private var pending = Subscribers.Demand.none // 0xC8
|
||||
|
||||
internal init(downstream: Downstream, count: Int) {
|
||||
self.downstream = downstream
|
||||
self.count = count
|
||||
self.subscriptions = Array(repeating: nil, count: count)
|
||||
self.buffers = Array(repeating: nil, count: count)
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
downstreamLock.deallocate()
|
||||
}
|
||||
|
||||
private func receive(subscription: Subscription, _ index: Int) {
|
||||
lock.lock()
|
||||
guard subscriptions[index] == nil else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
subscriptions[index] = subscription
|
||||
let demand = self.demand
|
||||
lock.unlock()
|
||||
subscription.request(demand == .unlimited ? .unlimited : .max(1))
|
||||
}
|
||||
|
||||
private func receive(_ input: Input, _ index: Int) -> Subscribers.Demand {
|
||||
func lockedSendValueDownstream() -> Subscribers.Demand {
|
||||
recursive = true
|
||||
lock.unlock()
|
||||
downstreamLock.lock()
|
||||
let newDemand = downstream.receive(input)
|
||||
downstreamLock.unlock()
|
||||
lock.lock()
|
||||
recursive = false
|
||||
return newDemand
|
||||
}
|
||||
|
||||
lock.lock()
|
||||
if demand == .unlimited {
|
||||
let newDemand = lockedSendValueDownstream()
|
||||
lock.unlock()
|
||||
return newDemand
|
||||
}
|
||||
if demand == .none {
|
||||
buffers[index] = input
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
demand -= 1
|
||||
let newDemand = lockedSendValueDownstream()
|
||||
demand += newDemand + pending
|
||||
pending = .none
|
||||
lock.unlock()
|
||||
return .max(1)
|
||||
}
|
||||
|
||||
private func receive(completion: Subscribers.Completion<Failure>, _ index: Int) {
|
||||
func lockedSendCompletionDownstream() {
|
||||
recursive = true
|
||||
lock.unlock()
|
||||
downstreamLock.lock()
|
||||
downstream.receive(completion: completion)
|
||||
downstreamLock.unlock()
|
||||
lock.lock()
|
||||
recursive = false
|
||||
}
|
||||
|
||||
lock.lock()
|
||||
switch completion {
|
||||
case .finished:
|
||||
upstreamFinished += 1
|
||||
subscriptions[index] = nil
|
||||
// TODO: Test both conditions.
|
||||
// When receiving subscription twice, the second time
|
||||
// upstreamFinished != count
|
||||
guard upstreamFinished == count,
|
||||
subscriptions.allSatisfy({ $0 == nil }) else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
finished = true
|
||||
lockedSendCompletionDownstream()
|
||||
lock.unlock()
|
||||
case .failure:
|
||||
if terminated {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
terminated = true
|
||||
let subscriptions = self.subscriptions
|
||||
self.subscriptions = Array(repeating: nil, count: subscriptions.count)
|
||||
lock.unlock()
|
||||
for (i, subscription) in subscriptions.enumerated() where i != index {
|
||||
subscription?.cancel()
|
||||
}
|
||||
lock.lock()
|
||||
lockedSendCompletionDownstream()
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
internal func request(_ demand: Subscribers.Demand) {
|
||||
lock.lock()
|
||||
// TODO: Test all conditions
|
||||
if terminated || finished || demand == .none || self.demand == .unlimited {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
if recursive {
|
||||
pending += demand
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
if demand == .unlimited {
|
||||
// loc_6a5b1
|
||||
self.demand = .unlimited
|
||||
}
|
||||
|
||||
// TODO: Unimplemented
|
||||
lock.unlock()
|
||||
}
|
||||
|
||||
internal func cancel() {
|
||||
// TODO: Unimplemented
|
||||
}
|
||||
|
||||
internal var description: String { return "Merge" }
|
||||
|
||||
internal var customMirror: Mirror {
|
||||
return Mirror(self, children: EmptyCollection())
|
||||
}
|
||||
|
||||
internal var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers._Merged {
|
||||
internal struct Side
|
||||
: Subscriber,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
{
|
||||
private let index: Int
|
||||
private let merger: Publishers._Merged<Input, Failure, Downstream>
|
||||
|
||||
internal let combineIdentifier = CombineIdentifier()
|
||||
|
||||
internal init(index: Int,
|
||||
merger: Publishers._Merged<Input, Failure, Downstream>) {
|
||||
self.index = index
|
||||
self.merger = merger
|
||||
}
|
||||
|
||||
internal func receive(subscription: Subscription) {
|
||||
merger.receive(subscription: subscription, index)
|
||||
}
|
||||
|
||||
internal func receive(_ input: Input) -> Subscribers.Demand {
|
||||
return merger.receive(input, index)
|
||||
}
|
||||
|
||||
internal func receive(completion: Subscribers.Completion<Failure>) {
|
||||
merger.receive(completion: completion, index)
|
||||
}
|
||||
|
||||
internal var description: String { return "Merge" }
|
||||
|
||||
internal var customMirror: Mirror {
|
||||
let children = CollectionOfOne<Mirror.Child>(
|
||||
("parentSubscription", merger.combineIdentifier)
|
||||
)
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
internal var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,6 @@
|
||||
// Created by Sergej Jaskiewicz on 12.11.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
/// A publisher that allows for recording a series of inputs and a completion for later
|
||||
/// playback to each subscriber.
|
||||
public struct Record<Output, Failure: Error>: Publisher {
|
||||
|
||||
@@ -330,6 +330,7 @@ extension Subscribers {
|
||||
|
||||
/// Returns `true` if `lhs` and `rhs` are not equal. `.unlimited` is not equal to
|
||||
/// any integer.
|
||||
@inlinable
|
||||
public static func != (lhs: Demand, rhs: Int) -> Bool {
|
||||
if lhs == .unlimited {
|
||||
return true
|
||||
@@ -340,6 +341,7 @@ extension Subscribers {
|
||||
|
||||
/// Returns `true` if `lhs` and `rhs` are equal. `.unlimited` is not equal to any
|
||||
/// integer.
|
||||
@inlinable
|
||||
public static func == (lhs: Int, rhs: Demand) -> Bool {
|
||||
if rhs == .unlimited {
|
||||
return false
|
||||
@@ -350,6 +352,7 @@ extension Subscribers {
|
||||
|
||||
/// Returns `true` if `lhs` and `rhs` are not equal. `.unlimited` is not equal to
|
||||
/// any integer.
|
||||
@inlinable
|
||||
public static func != (lhs: Int, rhs: Demand) -> Bool {
|
||||
if rhs == .unlimited {
|
||||
return true
|
||||
@@ -358,8 +361,13 @@ extension Subscribers {
|
||||
}
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public static func == (lhs: Demand, rhs: Demand) -> Bool {
|
||||
return lhs.rawValue == rhs.rawValue
|
||||
}
|
||||
|
||||
/// Returns the number of requested values, or `nil` if `.unlimited`.
|
||||
public var max: Int? {
|
||||
@inlinable public var max: Int? {
|
||||
if self == .unlimited {
|
||||
return nil
|
||||
} else {
|
||||
|
||||
@@ -50,8 +50,12 @@ extension DispatchQueue {
|
||||
/// - Parameter other: Another dispatch queue time.
|
||||
/// - Returns: The time interval between this time and the provided time.
|
||||
public func distance(to other: SchedulerTimeType) -> Stride {
|
||||
let start = dispatchTime.rawValue
|
||||
let end = other.dispatchTime.rawValue
|
||||
return .nanoseconds(
|
||||
dispatchTime.rawValue.distance(to: other.dispatchTime.rawValue)
|
||||
end >= start
|
||||
? Int(Int64(bitPattern: end) - Int64(bitPattern: start))
|
||||
: -Int(Int64(bitPattern: start) - Int64(bitPattern: end))
|
||||
)
|
||||
}
|
||||
|
||||
@@ -62,7 +66,9 @@ extension DispatchQueue {
|
||||
/// - Returns: A dispatch queue time advanced by the given
|
||||
/// interval from this instance’s time.
|
||||
public func advanced(by stride: Stride) -> SchedulerTimeType {
|
||||
return .init(dispatchTime + stride.timeInterval)
|
||||
return stride.magnitude == .max
|
||||
? .init(.distantFuture)
|
||||
: .init(dispatchTime + stride.timeInterval)
|
||||
}
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
@@ -125,13 +131,52 @@ extension DispatchQueue {
|
||||
self = .microseconds(microseconds)
|
||||
case .nanoseconds(let nanoseconds):
|
||||
self = .nanoseconds(nanoseconds)
|
||||
// This dance is to avoid the warning 'default will never be executed'
|
||||
// on non-Darwin platforms.
|
||||
// There really shouldn't be a warning.
|
||||
// See https://forums.swift.org/t/unknown-default-produces-a-warning-on-linux-with-non-frozen-enum/31687
|
||||
//
|
||||
// Thanks to Jeremy David Giesbrecht for suggesting this workaround.
|
||||
#if canImport(Darwin)
|
||||
case .never:
|
||||
fallthrough
|
||||
@unknown default:
|
||||
self = .nanoseconds(.max)
|
||||
@unknown default:
|
||||
self.init(__guessFromUnknown: timeInterval)
|
||||
#else
|
||||
default:
|
||||
if case .never = timeInterval {
|
||||
self = .nanoseconds(.max)
|
||||
} else {
|
||||
self.init(__guessFromUnknown: timeInterval)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
public // testable
|
||||
init(__guessFromUnknown timeInterval: DispatchTimeInterval) {
|
||||
// Let's take some reference time,
|
||||
// add `timeInterval` to it, take the `rawValue` from the result
|
||||
// and subtract the `rawValue` of the reference time.
|
||||
//
|
||||
// We won't be able to provide the exact implementation though,
|
||||
// because something will definitely overflow.
|
||||
//
|
||||
// However, we can try to support as wide a range of values
|
||||
// as possible.
|
||||
//
|
||||
// By trial and error I got that the `rawValue` of `UInt64.max / 13`
|
||||
// gives us propably the widest range of supported values:
|
||||
// from `Int.min / 6.5` to `Int.max / 2.889` nanoseconds.
|
||||
// That's with Int being 64 bits. Since here only UInt64 can overflow,
|
||||
// when Int is 32 bits, we don't have this issue.
|
||||
// It should be more than enough.
|
||||
|
||||
let referenceTime = DispatchTime(uptimeNanoseconds: .max / 13)
|
||||
self = SchedulerTimeType(referenceTime)
|
||||
.distance(to: SchedulerTimeType(referenceTime + timeInterval))
|
||||
}
|
||||
|
||||
/// Creates a dispatch queue time interval from a floating-point
|
||||
/// seconds value.
|
||||
///
|
||||
@@ -191,15 +236,15 @@ extension DispatchQueue {
|
||||
}
|
||||
|
||||
public static func seconds(_ value: Int) -> Stride {
|
||||
return Stride(magnitude: value * 1_000_000_000)
|
||||
return Stride(magnitude: clampedIntProduct(value, 1_000_000_000))
|
||||
}
|
||||
|
||||
public static func milliseconds(_ value: Int) -> Stride {
|
||||
return Stride(magnitude: value * 1_000_000)
|
||||
return Stride(magnitude: clampedIntProduct(value, 1_000_000))
|
||||
}
|
||||
|
||||
public static func microseconds(_ value: Int) -> Stride {
|
||||
return Stride(magnitude: value * 1_000)
|
||||
return Stride(magnitude: clampedIntProduct(value, 1_000))
|
||||
}
|
||||
|
||||
public static func nanoseconds(_ value: Int) -> Stride {
|
||||
@@ -336,3 +381,18 @@ extension DispatchQueue: OpenCombine.Scheduler {
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// This function is taken from swift-corlibs-libdispatch:
|
||||
// https://github.com/apple/swift-corelibs-libdispatch/blob/c992dacf3ca114806e6ac9ffc9113b19255be9fe/src/swift/Time.swift#L134-L144
|
||||
//
|
||||
// Returns m1 * m2, clamped to the range [Int.min, Int.max].
|
||||
// Because of the way this function is used, we can always assume
|
||||
// that m2 > 0.
|
||||
private func clampedIntProduct(_ lhs: Int, _ rhs: Int) -> Int {
|
||||
assert(rhs > 0, "multiplier must be positive")
|
||||
let (result, overflow) = lhs.multipliedReportingOverflow(by: rhs)
|
||||
if overflow {
|
||||
return lhs > 0 ? .max : .min
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
//
|
||||
// Locking.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 10.12.2019.
|
||||
//
|
||||
|
||||
#if canImport(COpenCombineHelpers)
|
||||
import COpenCombineHelpers
|
||||
#endif
|
||||
|
||||
import OpenCombine
|
||||
|
||||
internal typealias UnfairLock = __UnfairLock
|
||||
internal typealias UnfairRecursiveLock = __UnfairRecursiveLock
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// Violations.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 13.12.2019.
|
||||
//
|
||||
|
||||
import OpenCombine
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// JSONEncoder.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 10.10.2019.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import OpenCombine
|
||||
|
||||
extension JSONEncoder: TopLevelEncoder {
|
||||
public typealias Output = Data
|
||||
}
|
||||
|
||||
extension JSONDecoder: TopLevelDecoder {
|
||||
public typealias Input = Data
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
//
|
||||
// NotificationCenter.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 10.10.2019.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import OpenCombine
|
||||
|
||||
extension NotificationCenter {
|
||||
|
||||
/// A namespace for disambiguation when both OpenCombine and Foundation are imported.
|
||||
///
|
||||
/// Foundation extends `NotificationCenter` with new methods and nested types.
|
||||
/// If you import both OpenCombine and Foundation, you will not be able
|
||||
/// to write `NotificationCenter.Publisher`,
|
||||
/// because Swift is unable to understand which `Publisher`
|
||||
/// you're referring to — the one declared in Foundation or in OpenCombine.
|
||||
///
|
||||
/// So you have to write `NotificationCenter.OCombine.Publisher`.
|
||||
///
|
||||
/// 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 {
|
||||
|
||||
public let center: NotificationCenter
|
||||
|
||||
public init(_ center: NotificationCenter) {
|
||||
self.center = center
|
||||
}
|
||||
|
||||
/// A publisher that emits elements when broadcasting notifications.
|
||||
public struct Publisher: OpenCombine.Publisher {
|
||||
|
||||
public typealias Output = Notification
|
||||
|
||||
public typealias Failure = Never
|
||||
|
||||
/// The notification center this publisher uses as a source.
|
||||
public let center: NotificationCenter
|
||||
|
||||
/// The name of notifications published by this publisher.
|
||||
public let name: Notification.Name
|
||||
|
||||
/// The object posting the named notfication.
|
||||
public let object: AnyObject?
|
||||
|
||||
/// Creates a publisher that emits events when broadcasting notifications.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - center: The notification center to publish notifications for.
|
||||
/// - name: The name of the notification to publish.
|
||||
/// - object: The object posting the named notfication. If `nil`,
|
||||
/// the publisher emits elements for any object producing a notification
|
||||
/// with the given name.
|
||||
public init(center: NotificationCenter,
|
||||
name: Notification.Name,
|
||||
object: AnyObject? = nil) {
|
||||
self.center = center
|
||||
self.name = name
|
||||
self.object = object
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Failure == Never, Downstream.Input == Notification
|
||||
{
|
||||
let subscription = Notification.Subscription(center: center,
|
||||
name: name,
|
||||
object: object,
|
||||
downstream: subscriber)
|
||||
subscriber.receive(subscription: subscription)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a publisher that emits events when broadcasting notifications.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - name: The name of the notification to publish.
|
||||
/// - object: The object posting the named notfication. If `nil`, the publisher
|
||||
/// emits elements for any object producing a notification with the given
|
||||
/// name.
|
||||
/// - Returns: A publisher that emits events when broadcasting notifications.
|
||||
public func publisher(for name: Notification.Name,
|
||||
object: AnyObject? = nil) -> Publisher {
|
||||
return .init(center: center, name: name, object: object)
|
||||
}
|
||||
}
|
||||
|
||||
#if !canImport(Combine)
|
||||
/// A publisher that emits elements when broadcasting notifications.
|
||||
public typealias Publisher = OCombine.Publisher
|
||||
#endif
|
||||
}
|
||||
|
||||
extension NotificationCenter {
|
||||
|
||||
/// A namespace for disambiguation when both OpenCombine and Foundation are imported.
|
||||
///
|
||||
/// Foundation extends `NotificationCenter` with new methods and nested types.
|
||||
/// If you import both OpenCombine and Foundation, you will not be able
|
||||
/// to write `NotificationCenter.default.publisher(for: name)`,
|
||||
/// because Swift is unable to understand which `publisher` method
|
||||
/// you're referring to — the one declared in Foundation or in OpenCombine.
|
||||
///
|
||||
/// So you have to write `NotificationCenter.default.ocombine.publisher(for: name)`.
|
||||
///
|
||||
/// 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 .init(self) }
|
||||
|
||||
#if !canImport(Combine)
|
||||
/// Returns a publisher that emits events when broadcasting notifications.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - name: The name of the notification to publish.
|
||||
/// - object: The object posting the named notfication. If `nil`, the publisher
|
||||
/// emits elements for any object producing a notification with the given name.
|
||||
/// - Returns: A publisher that emits events when broadcasting notifications.
|
||||
public func publisher(for name: Notification.Name,
|
||||
object: AnyObject? = nil) -> OCombine.Publisher {
|
||||
return ocombine.publisher(for: name, object: object)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
extension NotificationCenter.OCombine.Publisher: Equatable {
|
||||
public static func == (lhs: NotificationCenter.OCombine.Publisher,
|
||||
rhs: NotificationCenter.OCombine.Publisher) -> Bool {
|
||||
return lhs.center == rhs.center &&
|
||||
lhs.name == rhs.name &&
|
||||
lhs.object === rhs.object
|
||||
}
|
||||
}
|
||||
|
||||
extension Notification {
|
||||
fileprivate final class Subscription<Downstream: Subscriber>
|
||||
: OpenCombine.Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Notification, Downstream.Failure == Never
|
||||
{
|
||||
private let lock = UnfairLock.allocate()
|
||||
|
||||
fileprivate let downstreamLock = UnfairRecursiveLock.allocate()
|
||||
|
||||
fileprivate var demand = Subscribers.Demand.none
|
||||
|
||||
private var center: NotificationCenter?
|
||||
|
||||
private let name: Name
|
||||
|
||||
private var object: AnyObject?
|
||||
|
||||
private var observation: AnyObject?
|
||||
|
||||
fileprivate init(center: NotificationCenter,
|
||||
name: Notification.Name,
|
||||
object: AnyObject?,
|
||||
downstream: Downstream) {
|
||||
self.center = center
|
||||
self.name = name
|
||||
self.object = object
|
||||
self.observation = center
|
||||
.addObserver(forName: name, object: object, queue: nil) { [weak self] in
|
||||
self?.didReceiveNotification($0, downstream: downstream)
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
downstreamLock.deallocate()
|
||||
}
|
||||
|
||||
private func didReceiveNotification(_ notification: Notification,
|
||||
downstream: Downstream) {
|
||||
lock.lock()
|
||||
guard demand > 0 else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
demand -= 1
|
||||
lock.unlock()
|
||||
downstreamLock.lock()
|
||||
let newDemand = downstream.receive(notification)
|
||||
downstreamLock.unlock()
|
||||
lock.lock()
|
||||
demand += newDemand
|
||||
lock.unlock()
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
lock.lock()
|
||||
self.demand += demand
|
||||
lock.unlock()
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
guard let center = self.center, let observation = self.observation else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
self.center = nil
|
||||
self.object = nil
|
||||
self.observation = nil
|
||||
lock.unlock()
|
||||
center.removeObserver(observation)
|
||||
}
|
||||
|
||||
fileprivate var description: String { return "NotificationCenter Observer" }
|
||||
|
||||
fileprivate var customMirror: Mirror {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
let children: [Mirror.Child] = [
|
||||
("center", center as Any),
|
||||
("name", name),
|
||||
("object", object as Any),
|
||||
("demand", demand)
|
||||
]
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
fileprivate var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// PropertyListEncoder.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 10.12.2019.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import OpenCombine
|
||||
|
||||
extension PropertyListEncoder: TopLevelEncoder {
|
||||
public typealias Output = Data
|
||||
}
|
||||
|
||||
extension PropertyListDecoder: TopLevelDecoder {
|
||||
public typealias Input = Data
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
//
|
||||
// URLSession.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 13.12.2019.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
#if canImport(FoundationNetworking)
|
||||
import FoundationNetworking
|
||||
#endif
|
||||
|
||||
import OpenCombine
|
||||
|
||||
extension URLSession {
|
||||
|
||||
/// A namespace for disambiguation when both OpenCombine and Foundation are imported.
|
||||
///
|
||||
/// Foundation extends `URLSession` with new methods and nested types.
|
||||
/// If you import both OpenCombine and Foundation, you will not be able
|
||||
/// to write `URLSession.DataTaskPublisher`,
|
||||
/// because Swift is unable to understand which `DataTaskPublisher`
|
||||
/// you're referring to — the one declared in Foundation or in OpenCombine.
|
||||
///
|
||||
/// So you have to write `URLSession.OCombine.DataTaskPublisher`.
|
||||
///
|
||||
/// 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 {
|
||||
|
||||
public let session: URLSession
|
||||
|
||||
public init(_ session: URLSession) {
|
||||
self.session = session
|
||||
}
|
||||
|
||||
public struct DataTaskPublisher: Publisher {
|
||||
|
||||
public typealias Output = (data: Data, response: URLResponse)
|
||||
|
||||
public typealias Failure = URLError
|
||||
|
||||
public let request: URLRequest
|
||||
|
||||
public let session: URLSession
|
||||
|
||||
public init(request: URLRequest, session: URLSession) {
|
||||
self.request = request
|
||||
self.session = session
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Failure == Failure, Downstream.Input == Output
|
||||
{
|
||||
let subscription = Inner(parent: self, downstream: subscriber)
|
||||
subscriber.receive(subscription: subscription)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a publisher that wraps a URL session data task for a given URL.
|
||||
///
|
||||
/// The publisher publishes data when the task completes, or terminates if
|
||||
/// the task fails with an error.
|
||||
///
|
||||
/// - Parameter url: The URL for which to create a data task.
|
||||
/// - Returns: A publisher that wraps a data task for the URL.
|
||||
public func dataTaskPublisher(for url: URL) -> DataTaskPublisher {
|
||||
return dataTaskPublisher(for: URLRequest(url: url))
|
||||
}
|
||||
|
||||
/// Returns a publisher that wraps a URL session data task for a given
|
||||
/// URL request.
|
||||
///
|
||||
/// The publisher publishes data when the task completes, or terminates if
|
||||
/// the task fails with an error.
|
||||
///
|
||||
/// - Parameter request: The URL request for which to create a data task.
|
||||
/// - Returns: A publisher that wraps a data task for the URL request.
|
||||
public func dataTaskPublisher(for request: URLRequest) -> DataTaskPublisher {
|
||||
return .init(request: request, session: session)
|
||||
}
|
||||
}
|
||||
|
||||
#if !canImport(Combine)
|
||||
public typealias DataTaskPublisher = OCombine.DataTaskPublisher
|
||||
#endif
|
||||
}
|
||||
|
||||
extension URLSession {
|
||||
|
||||
/// A namespace for disambiguation when both OpenCombine and Foundation are imported.
|
||||
///
|
||||
/// Foundation extends `URLSession` with new methods and nested types.
|
||||
/// If you import both OpenCombine and Foundation, you will not be able
|
||||
/// to write `URLSession.shared.dataTaskPublisher(for: url)`,
|
||||
/// because Swift is unable to understand which `dataTaskPublisher` method
|
||||
/// you're referring to — the one declared in Foundation or in OpenCombine.
|
||||
///
|
||||
/// So you have to write `URLSession.shared.ocombine.dataTaskPublisher(for: url)`.
|
||||
///
|
||||
/// 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 .init(self) }
|
||||
|
||||
#if !canImport(Combine)
|
||||
/// Returns a publisher that wraps a URL session data task for a given URL.
|
||||
///
|
||||
/// The publisher publishes data when the task completes, or terminates if the task
|
||||
/// fails with an error.
|
||||
///
|
||||
/// - Parameter url: The URL for which to create a data task.
|
||||
/// - Returns: A publisher that wraps a data task for the URL.
|
||||
public func dataTaskPublisher(for url: URL) -> DataTaskPublisher {
|
||||
return ocombine.dataTaskPublisher(for: url)
|
||||
}
|
||||
|
||||
/// Returns a publisher that wraps a URL session data task for a given URL request.
|
||||
///
|
||||
/// The publisher publishes data when the task completes, or terminates if the task
|
||||
/// fails with an error.
|
||||
///
|
||||
/// - Parameter request: The URL request for which to create a data task.
|
||||
/// - Returns: A publisher that wraps a data task for the URL request.
|
||||
public func dataTaskPublisher(for request: URLRequest) -> DataTaskPublisher {
|
||||
return ocombine.dataTaskPublisher(for: request)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
extension URLSession.OCombine.DataTaskPublisher {
|
||||
private class Inner<Downstream: Subscriber>
|
||||
: Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == (data: Data, response: URLResponse),
|
||||
Downstream.Failure == URLError
|
||||
{
|
||||
private let lock = UnfairLock.allocate()
|
||||
|
||||
private var parent: URLSession.OCombine.DataTaskPublisher?
|
||||
|
||||
private var downstream: Downstream?
|
||||
|
||||
private var demand = Subscribers.Demand.none
|
||||
|
||||
private var task: URLSessionDataTask?
|
||||
|
||||
fileprivate init(parent: URLSession.OCombine.DataTaskPublisher,
|
||||
downstream: Downstream) {
|
||||
self.parent = parent
|
||||
self.downstream = downstream
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
demand.assertNonZero()
|
||||
lock.lock()
|
||||
guard let parent = self.parent else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
if self.task == nil {
|
||||
task = parent.session.dataTask(with: parent.request,
|
||||
completionHandler: handleResponse)
|
||||
}
|
||||
self.demand += demand
|
||||
let task = self.task
|
||||
lock.unlock()
|
||||
task?.resume()
|
||||
}
|
||||
|
||||
private func handleResponse(data: Data?, response: URLResponse?, error: Error?) {
|
||||
lock.lock()
|
||||
guard demand > 0, parent != nil, let downstream = self.downstream else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
lockedTerminate()
|
||||
lock.unlock()
|
||||
switch (data, response, error) {
|
||||
case let (data, response?, nil):
|
||||
_ = downstream.receive((data ?? Data(), response))
|
||||
downstream.receive(completion: .finished)
|
||||
case let (_, _, error as URLError):
|
||||
downstream.receive(completion: .failure(error))
|
||||
default:
|
||||
downstream.receive(completion: .failure(URLError(.unknown)))
|
||||
}
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
guard parent != nil else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
let task = self.task
|
||||
lockedTerminate()
|
||||
lock.unlock()
|
||||
task?.cancel()
|
||||
}
|
||||
|
||||
private func lockedTerminate() {
|
||||
parent = nil
|
||||
downstream = nil
|
||||
demand = .none
|
||||
task = nil
|
||||
}
|
||||
|
||||
var description: String { return "DataTaskPublisher" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
let children: [Mirror.Child] = [
|
||||
("task", task as Any),
|
||||
("downstream", downstream as Any),
|
||||
("parent", parent as Any),
|
||||
("demand", demand)
|
||||
]
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
@@ -23,13 +23,34 @@ final class DispatchQueueSchedulerTests: XCTestCase {
|
||||
func testSchedulerTimeTypeDistance() {
|
||||
let time1 = Scheduler.SchedulerTimeType(.init(uptimeNanoseconds: 10000))
|
||||
let time2 = Scheduler.SchedulerTimeType(.init(uptimeNanoseconds: 10431))
|
||||
let distantFuture = Scheduler.SchedulerTimeType(.distantFuture)
|
||||
let notSoDistantFuture = Scheduler.SchedulerTimeType(
|
||||
DispatchTime(
|
||||
uptimeNanoseconds: DispatchTime.distantFuture.uptimeNanoseconds - 1024
|
||||
)
|
||||
)
|
||||
|
||||
XCTAssertEqual(time1.distance(to: time2), .nanoseconds(431))
|
||||
XCTAssertEqual(time2.distance(to: time1), .nanoseconds(-431))
|
||||
|
||||
XCTAssertEqual(time1.distance(to: distantFuture), .nanoseconds(-10001))
|
||||
XCTAssertEqual(distantFuture.distance(to: time1), .nanoseconds(10001))
|
||||
XCTAssertEqual(time2.distance(to: distantFuture), .nanoseconds(-10432))
|
||||
XCTAssertEqual(distantFuture.distance(to: time2), .nanoseconds(10432))
|
||||
|
||||
XCTAssertEqual(time1.distance(to: notSoDistantFuture), .nanoseconds(-11025))
|
||||
XCTAssertEqual(notSoDistantFuture.distance(to: time1), .nanoseconds(11025))
|
||||
XCTAssertEqual(time2.distance(to: notSoDistantFuture), .nanoseconds(-11456))
|
||||
XCTAssertEqual(notSoDistantFuture.distance(to: time2), .nanoseconds(11456))
|
||||
|
||||
XCTAssertEqual(distantFuture.distance(to: distantFuture), .nanoseconds(0))
|
||||
XCTAssertEqual(notSoDistantFuture.distance(to: notSoDistantFuture),
|
||||
.nanoseconds(0))
|
||||
}
|
||||
|
||||
func testSchedulerTimeTypeAdvanced() {
|
||||
let time = Scheduler.SchedulerTimeType(.init(uptimeNanoseconds: 10000))
|
||||
let beginningOfTime = Scheduler.SchedulerTimeType(.init(uptimeNanoseconds: 1))
|
||||
let stride1 = Scheduler.SchedulerTimeType.Stride.nanoseconds(431)
|
||||
let stride2 = Scheduler.SchedulerTimeType.Stride.nanoseconds(-220)
|
||||
|
||||
@@ -38,6 +59,12 @@ final class DispatchQueueSchedulerTests: XCTestCase {
|
||||
|
||||
XCTAssertEqual(time.advanced(by: stride2),
|
||||
Scheduler.SchedulerTimeType(.init(uptimeNanoseconds: 9780)))
|
||||
|
||||
XCTAssertEqual(time.advanced(by: .nanoseconds(.max)).dispatchTime,
|
||||
.distantFuture)
|
||||
|
||||
XCTAssertEqual(beginningOfTime.advanced(by: .nanoseconds(-1000)).dispatchTime,
|
||||
DispatchTime(uptimeNanoseconds: 1))
|
||||
}
|
||||
|
||||
func testSchedulerTimeTypeEquatable() {
|
||||
@@ -84,41 +111,145 @@ final class DispatchQueueSchedulerTests: XCTestCase {
|
||||
// MARK: - Scheduler.SchedulerTimeType.Stride
|
||||
|
||||
func testStrideToDispatchTimeInterval() {
|
||||
typealias Stride = Scheduler.SchedulerTimeType.Stride
|
||||
|
||||
switch (Stride.seconds(2).timeInterval,
|
||||
Stride.milliseconds(2).timeInterval,
|
||||
Stride.microseconds(2).timeInterval,
|
||||
Stride.nanoseconds(2).timeInterval) {
|
||||
Stride.nanoseconds(2).timeInterval,
|
||||
Stride.nanoseconds(.max).timeInterval) {
|
||||
case (.nanoseconds(2_000_000_000),
|
||||
.nanoseconds(2_000_000),
|
||||
.nanoseconds(2_000),
|
||||
.nanoseconds(2)):
|
||||
.nanoseconds(2),
|
||||
.nanoseconds(.max)):
|
||||
break // pass
|
||||
case let intervals:
|
||||
XCTFail("Unexpected DispatchTimeInterval: \(intervals)")
|
||||
}
|
||||
}
|
||||
|
||||
func testStrideFromDispatchTimeInterval() {
|
||||
typealias Stride = Scheduler.SchedulerTimeType.Stride
|
||||
|
||||
func testStrideFromDispatchTimeInterval() throws {
|
||||
XCTAssertEqual(Stride(.seconds(2)).magnitude, 2_000_000_000)
|
||||
XCTAssertEqual(Stride(.milliseconds(2)).magnitude, 2_000_000)
|
||||
XCTAssertEqual(Stride(.microseconds(2)).magnitude, 2_000)
|
||||
XCTAssertEqual(Stride(.nanoseconds(2)).magnitude, 2)
|
||||
|
||||
XCTAssertEqual(Stride(.never).magnitude, .max)
|
||||
XCTAssertEqual(Stride(.nanoseconds(.max)).magnitude, .max)
|
||||
XCTAssertEqual(Stride(.nanoseconds(.min)).magnitude, .min)
|
||||
XCTAssertEqual(Stride(.microseconds(.max)).magnitude, .max)
|
||||
XCTAssertEqual(Stride(.microseconds(.min)).magnitude, .min)
|
||||
XCTAssertEqual(Stride(.milliseconds(.max)).magnitude, .max)
|
||||
XCTAssertEqual(Stride(.milliseconds(.min)).magnitude, .min)
|
||||
XCTAssertEqual(Stride(.seconds(.max)).magnitude, .max)
|
||||
XCTAssertEqual(Stride(.seconds(.min)).magnitude, .min)
|
||||
}
|
||||
|
||||
func testStrideFromUnknownDispatchTimeIntervalCase() {
|
||||
// Here we're testing out internal API that is not present in Combine.
|
||||
// Although we prefer only testing public APIs, this case is special.
|
||||
let makeStride: (DispatchTimeInterval) -> Stride
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
makeStride = Stride.init(_:)
|
||||
#else
|
||||
makeStride = Stride.init(__guessFromUnknown:)
|
||||
#endif
|
||||
|
||||
#if arch(x86_64) || arch(arm64) || arch(s390x) || arch(powerpc64) || arch(powerpc64le)
|
||||
// 64-bit platforms
|
||||
let minNanoseconds = -0x13B13B13B13B13B0 // Int64.min / 6.5
|
||||
let maxNanoseconds = 0x2C4EC4EC4EC4EC4D // Int64.max / 2.889
|
||||
#elseif arch(i386) || arch(arm)
|
||||
// 32-bit platforms
|
||||
let minNanoseconds = Int.min + 1
|
||||
let maxNanoseconds = Int.max
|
||||
#else
|
||||
#error("This architecture isn't known. Add it to the 32-bit or 64-bit line.")
|
||||
#endif
|
||||
XCTAssertEqual(makeStride(.nanoseconds(minNanoseconds)).magnitude, minNanoseconds)
|
||||
XCTAssertEqual(makeStride(.nanoseconds(-128)).magnitude, -128)
|
||||
XCTAssertEqual(makeStride(.nanoseconds(-57)).magnitude, -57)
|
||||
XCTAssertEqual(makeStride(.nanoseconds(-33)).magnitude, -33)
|
||||
XCTAssertEqual(makeStride(.nanoseconds(-17)).magnitude, -17)
|
||||
XCTAssertEqual(makeStride(.nanoseconds(-8)).magnitude, -8)
|
||||
XCTAssertEqual(makeStride(.nanoseconds(-3)).magnitude, -3)
|
||||
XCTAssertEqual(makeStride(.nanoseconds(-1)).magnitude, -1)
|
||||
XCTAssertEqual(makeStride(.nanoseconds(0)).magnitude, 0)
|
||||
XCTAssertEqual(makeStride(.nanoseconds(1)).magnitude, 1)
|
||||
XCTAssertEqual(makeStride(.nanoseconds(3)).magnitude, 3)
|
||||
XCTAssertEqual(makeStride(.nanoseconds(8)).magnitude, 8)
|
||||
XCTAssertEqual(makeStride(.nanoseconds(17)).magnitude, 17)
|
||||
XCTAssertEqual(makeStride(.nanoseconds(33)).magnitude, 33)
|
||||
XCTAssertEqual(makeStride(.nanoseconds(57)).magnitude, 57)
|
||||
XCTAssertEqual(makeStride(.nanoseconds(128)).magnitude, 128)
|
||||
XCTAssertEqual(makeStride(.nanoseconds(maxNanoseconds)).magnitude, maxNanoseconds)
|
||||
|
||||
XCTAssertEqual(makeStride(.microseconds(-128)).magnitude, -128_000)
|
||||
XCTAssertEqual(makeStride(.microseconds(-57)).magnitude, -57_000)
|
||||
XCTAssertEqual(makeStride(.microseconds(-33)).magnitude, -33_000)
|
||||
XCTAssertEqual(makeStride(.microseconds(-17)).magnitude, -17_000)
|
||||
XCTAssertEqual(makeStride(.microseconds(-8)).magnitude, -8_000)
|
||||
XCTAssertEqual(makeStride(.microseconds(-3)).magnitude, -3_000)
|
||||
XCTAssertEqual(makeStride(.microseconds(-1)).magnitude, -1_000)
|
||||
XCTAssertEqual(makeStride(.microseconds(0)).magnitude, 0)
|
||||
XCTAssertEqual(makeStride(.microseconds(1)).magnitude, 1_000)
|
||||
XCTAssertEqual(makeStride(.microseconds(3)).magnitude, 3_000)
|
||||
XCTAssertEqual(makeStride(.microseconds(8)).magnitude, 8_000)
|
||||
XCTAssertEqual(makeStride(.microseconds(17)).magnitude, 17_000)
|
||||
XCTAssertEqual(makeStride(.microseconds(33)).magnitude, 33_000)
|
||||
XCTAssertEqual(makeStride(.microseconds(57)).magnitude, 57_000)
|
||||
XCTAssertEqual(makeStride(.microseconds(128)).magnitude, 128_000)
|
||||
|
||||
XCTAssertEqual(makeStride(.milliseconds(-128)).magnitude, -128_000_000)
|
||||
XCTAssertEqual(makeStride(.milliseconds(-57)).magnitude, -57_000_000)
|
||||
XCTAssertEqual(makeStride(.milliseconds(-33)).magnitude, -33_000_000)
|
||||
XCTAssertEqual(makeStride(.milliseconds(-17)).magnitude, -17_000_000)
|
||||
XCTAssertEqual(makeStride(.milliseconds(-8)).magnitude, -8_000_000)
|
||||
XCTAssertEqual(makeStride(.milliseconds(-3)).magnitude, -3_000_000)
|
||||
XCTAssertEqual(makeStride(.milliseconds(-1)).magnitude, -1_000_000)
|
||||
XCTAssertEqual(makeStride(.milliseconds(0)).magnitude, 0)
|
||||
XCTAssertEqual(makeStride(.milliseconds(1)).magnitude, 1_000_000)
|
||||
XCTAssertEqual(makeStride(.milliseconds(3)).magnitude, 3_000_000)
|
||||
XCTAssertEqual(makeStride(.milliseconds(8)).magnitude, 8_000_000)
|
||||
XCTAssertEqual(makeStride(.milliseconds(17)).magnitude, 17_000_000)
|
||||
XCTAssertEqual(makeStride(.milliseconds(33)).magnitude, 33_000_000)
|
||||
XCTAssertEqual(makeStride(.milliseconds(57)).magnitude, 57_000_000)
|
||||
XCTAssertEqual(makeStride(.milliseconds(128)).magnitude, 128_000_000)
|
||||
|
||||
XCTAssertEqual(makeStride(.seconds(-2)).magnitude, -2_000_000_000)
|
||||
XCTAssertEqual(makeStride(.seconds(-1)).magnitude, -1_000_000_000)
|
||||
XCTAssertEqual(makeStride(.seconds(0)).magnitude, 0)
|
||||
XCTAssertEqual(makeStride(.seconds(1)).magnitude, 1_000_000_000)
|
||||
XCTAssertEqual(makeStride(.seconds(2)).magnitude, 2_000_000_000)
|
||||
}
|
||||
|
||||
func testStrideFromNumericValue() {
|
||||
typealias Stride = Scheduler.SchedulerTimeType.Stride
|
||||
|
||||
XCTAssertEqual(Stride.seconds(1.2).magnitude, 1_200_000_000)
|
||||
XCTAssertEqual(Stride.seconds(2).magnitude, 2_000_000_000)
|
||||
XCTAssertEqual(Stride.milliseconds(2).magnitude, 2_000_000)
|
||||
XCTAssertEqual(Stride.microseconds(2).magnitude, 2_000)
|
||||
XCTAssertEqual(Stride.nanoseconds(2).magnitude, 2)
|
||||
|
||||
#if arch(x86_64) || arch(arm64) || arch(s390x) || arch(powerpc64) || arch(powerpc64le)
|
||||
// 64-bit platforms
|
||||
XCTAssertEqual(
|
||||
Stride.seconds(Double(Int.max) / 1_000_000_000 - 1).magnitude,
|
||||
9223372035854776320
|
||||
)
|
||||
#elseif arch(i386) || arch(arm)
|
||||
// 32-bit platforms
|
||||
XCTAssertEqual(
|
||||
Stride.seconds(Double(Int.max) / 1_000_000_000).magnitude,
|
||||
.max
|
||||
)
|
||||
#else
|
||||
#error("This architecture isn't known. Add it to the 32-bit or 64-bit line.")
|
||||
#endif
|
||||
|
||||
XCTAssertEqual(Stride.seconds(.max).magnitude, .max)
|
||||
XCTAssertEqual(Stride.milliseconds(.max).magnitude, .max)
|
||||
XCTAssertEqual(Stride.microseconds(.max).magnitude, .max)
|
||||
XCTAssertEqual(Stride.nanoseconds(.max).magnitude, .max)
|
||||
|
||||
XCTAssertEqual((1.2 as Stride).magnitude, 1_200_000_000)
|
||||
XCTAssertEqual((2 as Stride).magnitude, 2_000_000_000)
|
||||
|
||||
@@ -126,17 +257,33 @@ final class DispatchQueueSchedulerTests: XCTestCase {
|
||||
XCTAssertEqual(Stride(exactly: 871 as UInt64)?.magnitude, 871)
|
||||
}
|
||||
|
||||
func testStrideComparable() {
|
||||
typealias Stride = Scheduler.SchedulerTimeType.Stride
|
||||
func testStrideFromTooMuchSecondsCrashes() {
|
||||
assertCrashes {
|
||||
#if arch(x86_64) || arch(arm64) || arch(s390x) || arch(powerpc64) || arch(powerpc64le)
|
||||
// 64-bit platforms
|
||||
XCTAssertGreaterThan(
|
||||
Stride.seconds(Double(Int.max) / 1_000_000_000).magnitude,
|
||||
.max
|
||||
)
|
||||
#elseif arch(i386) || arch(arm)
|
||||
// 32-bit platforms
|
||||
XCTAssertGreaterThan(
|
||||
Stride.seconds(Double(Int.max) / 1_000_000_000 + 1).magnitude,
|
||||
.max
|
||||
)
|
||||
#else
|
||||
#error("This architecture isn't known. Add it to the 32-bit or 64-bit line.")
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
func testStrideComparable() {
|
||||
XCTAssertLessThan(Stride.nanoseconds(1), .nanoseconds(2))
|
||||
XCTAssertGreaterThan(Stride.nanoseconds(-2), .microseconds(-10))
|
||||
XCTAssertLessThan(Stride.milliseconds(2), .seconds(2))
|
||||
}
|
||||
|
||||
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, 18)
|
||||
@@ -196,8 +343,6 @@ final class DispatchQueueSchedulerTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testStrideAddition() {
|
||||
typealias Stride = Scheduler.SchedulerTimeType.Stride
|
||||
|
||||
XCTAssertEqual((Stride.nanoseconds(0) + .microseconds(2)).magnitude, 2000)
|
||||
XCTAssertEqual((Stride.nanoseconds(2) + .microseconds(0)).magnitude, 2)
|
||||
XCTAssertEqual((Stride.nanoseconds(7) + .nanoseconds(12)).magnitude, 19)
|
||||
@@ -243,8 +388,6 @@ final class DispatchQueueSchedulerTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testStrideSubtraction() {
|
||||
typealias Stride = Scheduler.SchedulerTimeType.Stride
|
||||
|
||||
XCTAssertEqual((Stride.nanoseconds(0) - .microseconds(2)).magnitude, -2000)
|
||||
XCTAssertEqual((Stride.nanoseconds(2) - .microseconds(0)).magnitude, 2)
|
||||
XCTAssertEqual((Stride.nanoseconds(7) - .nanoseconds(12)).magnitude, -5)
|
||||
@@ -290,8 +433,6 @@ final class DispatchQueueSchedulerTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testStrideCodable() throws {
|
||||
typealias Stride = Scheduler.SchedulerTimeType.Stride
|
||||
|
||||
let encoder = JSONEncoder()
|
||||
let decoder = JSONDecoder()
|
||||
|
||||
@@ -429,6 +570,9 @@ private let backgroundScheduler = DispatchQueue.global(qos: .background).ocombin
|
||||
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private typealias Stride = Scheduler.SchedulerTimeType.Stride
|
||||
|
||||
private struct KeyedWrapper<Value: Codable & Equatable>: Codable, Equatable {
|
||||
let value: Value
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
//
|
||||
// TopLevelDecoder+Extensions.swift
|
||||
//
|
||||
//
|
||||
// Created by Joseph Spadafora on 6/29/19.
|
||||
//
|
||||
|
||||
#if !OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Foundation
|
||||
import OpenCombine
|
||||
|
||||
extension JSONDecoder: TopLevelDecoder {}
|
||||
extension JSONEncoder: TopLevelEncoder {}
|
||||
|
||||
extension PropertyListDecoder: TopLevelDecoder {}
|
||||
extension PropertyListEncoder: TopLevelEncoder {}
|
||||
#endif
|
||||
@@ -0,0 +1,63 @@
|
||||
//
|
||||
// JSONDecoderTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 10.12.2019.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
import OpenCombineFoundation
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class JSONDecoderTests: XCTestCase {
|
||||
func testSuccessfullyDecode() {
|
||||
let decoder = JSONDecoder()
|
||||
let input = #"[{"success":true}]"#
|
||||
var actualOutput: [Subscribers.Completion<TestingError>]?
|
||||
var actualCompletion: Subscribers.Completion<Error>?
|
||||
let cancellable = Just(input)
|
||||
.map { Data($0.utf8) }
|
||||
.decode(type: [Subscribers.Completion<TestingError>].self, decoder: decoder)
|
||||
.sink(receiveCompletion: { actualCompletion = $0 },
|
||||
receiveValue: { actualOutput = $0 })
|
||||
switch actualCompletion {
|
||||
case .finished?:
|
||||
XCTAssertEqual(actualOutput, [.finished])
|
||||
case .failure(let error)?:
|
||||
XCTFail("Unexpected failure received: \(error)")
|
||||
case nil:
|
||||
XCTFail("Expected completion")
|
||||
}
|
||||
cancellable.cancel()
|
||||
}
|
||||
|
||||
func testDecodingFailure() {
|
||||
let decoder = JSONDecoder()
|
||||
let input = #"{"a":1,"b":2}"#
|
||||
var actualOutput: [Int]?
|
||||
var actualCompletion: Subscribers.Completion<Error>?
|
||||
let cancellable = Just(input)
|
||||
.map { Data($0.utf8) }
|
||||
.decode(type: [Int].self, decoder: decoder)
|
||||
.sink(receiveCompletion: { actualCompletion = $0 },
|
||||
receiveValue: { actualOutput = $0 })
|
||||
switch actualCompletion {
|
||||
case .finished?:
|
||||
XCTFail("Unexpected success")
|
||||
case .failure(DecodingError.typeMismatch)?:
|
||||
XCTAssertNil(actualOutput)
|
||||
case .failure(let error)?:
|
||||
XCTFail("Unexpected failure received: \(error)")
|
||||
case nil:
|
||||
XCTFail("Expected completion")
|
||||
}
|
||||
cancellable.cancel()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
//
|
||||
// JSONEncoderTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 10.12.2019.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
import OpenCombineFoundation
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class JSONEncoderTests: XCTestCase {
|
||||
|
||||
func testSuccessfullyEncode() {
|
||||
let encoder = JSONEncoder()
|
||||
let input = [Subscribers.Completion<TestingError>.finished]
|
||||
var actualOutput: String?
|
||||
var actualCompletion: Subscribers.Completion<Error>?
|
||||
|
||||
let cancellable = Just(input)
|
||||
.encode(encoder: encoder)
|
||||
.map { String(decoding: $0, as: UTF8.self) }
|
||||
.sink(receiveCompletion: { actualCompletion = $0 },
|
||||
receiveValue: { actualOutput = $0 })
|
||||
|
||||
switch actualCompletion {
|
||||
case .finished?:
|
||||
XCTAssertEqual(actualOutput, #"[{"success":true}]"#)
|
||||
case .failure(let error)?:
|
||||
XCTFail("Unexpected failure received: \(error)")
|
||||
case nil:
|
||||
XCTFail("Expected completion")
|
||||
}
|
||||
cancellable.cancel()
|
||||
}
|
||||
|
||||
func testEncodingFailure() {
|
||||
let encoder = JSONEncoder()
|
||||
let input = Double.nan
|
||||
var actualOutput: String?
|
||||
var actualCompletion: Subscribers.Completion<Error>?
|
||||
let cancellable = Just(input)
|
||||
.encode(encoder: encoder)
|
||||
.map { String(decoding: $0, as: UTF8.self) }
|
||||
.sink(receiveCompletion: { actualCompletion = $0 },
|
||||
receiveValue: { actualOutput = $0 })
|
||||
|
||||
switch actualCompletion {
|
||||
case .finished?:
|
||||
XCTFail("Unexpected success")
|
||||
case .failure(EncodingError.invalidValue)?:
|
||||
XCTAssertNil(actualOutput)
|
||||
case .failure(let error)?:
|
||||
XCTFail("Unexpected failure received: \(error)")
|
||||
case nil:
|
||||
XCTFail("Expected completion")
|
||||
}
|
||||
cancellable.cancel()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,625 @@
|
||||
//
|
||||
// NotificationCenterTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 10.12.2019.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
import OpenCombineFoundation
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class NotificationCenterTests: XCTestCase {
|
||||
|
||||
func testRequestingDemand() {
|
||||
|
||||
let initialDemands: [Subscribers.Demand?] = [
|
||||
nil,
|
||||
.max(1),
|
||||
.max(2),
|
||||
.max(10),
|
||||
.unlimited
|
||||
]
|
||||
|
||||
let subsequentDemands: [[Subscribers.Demand]] = [
|
||||
Array(repeating: .max(0), count: 5),
|
||||
Array(repeating: .max(1), count: 10),
|
||||
[.max(1), .max(0), .max(1), .max(0)],
|
||||
[.max(0), .max(1), .max(2)],
|
||||
[.unlimited, .max(1)]
|
||||
]
|
||||
|
||||
var numberOfInputsHistory: [Int] = []
|
||||
let expectedNumberOfInputsHistory = [
|
||||
0, 0, 0, 0, 0, 1, 11, 2, 1, 20, 2, 12, 4, 5, 20, 10, 20, 12, 13, 20, 20,
|
||||
20, 20, 20, 20
|
||||
]
|
||||
|
||||
for initialDemand in initialDemands {
|
||||
for subsequentDemand in subsequentDemands {
|
||||
|
||||
var i = 0
|
||||
|
||||
let center = TestNotificationCenter()
|
||||
let name = Notification.Name(rawValue: "testName")
|
||||
let publisher = makePublisher(center, for: name, object: nil)
|
||||
|
||||
let subscriber = TrackingSubscriberBase<Notification, Never>(
|
||||
receiveSubscription: { initialDemand.map($0.request) },
|
||||
receiveValue: { _ in
|
||||
defer { i += 1 }
|
||||
return i < subsequentDemand.endIndex ? subsequentDemand[i] : .none
|
||||
}
|
||||
)
|
||||
|
||||
XCTAssertEqual(subscriber.subscriptions.count, 0)
|
||||
XCTAssertEqual(subscriber.inputs.count, 0)
|
||||
XCTAssertEqual(subscriber.completions.count, 0)
|
||||
|
||||
publisher.subscribe(subscriber)
|
||||
|
||||
XCTAssertEqual(subscriber.subscriptions.count, 1)
|
||||
XCTAssertEqual(subscriber.inputs.count, 0)
|
||||
XCTAssertEqual(subscriber.completions.count, 0)
|
||||
|
||||
for _ in 0..<20 {
|
||||
center.post(name: name, object: TestObject.two)
|
||||
}
|
||||
|
||||
XCTAssertEqual(subscriber.subscriptions.count, 1)
|
||||
XCTAssertEqual(subscriber.completions.count, 0)
|
||||
|
||||
numberOfInputsHistory.append(subscriber.inputs.count)
|
||||
}
|
||||
}
|
||||
|
||||
XCTAssertEqual(numberOfInputsHistory, expectedNumberOfInputsHistory)
|
||||
}
|
||||
|
||||
func testBasicBehavior() throws {
|
||||
let center = TestNotificationCenter()
|
||||
let name = Notification.Name(rawValue: "testName")
|
||||
let publisher = makePublisher(center, for: name, object: TestObject.one)
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriberBase<Notification, Never>(
|
||||
receiveSubscription: { downstreamSubscription = $0 }
|
||||
)
|
||||
|
||||
XCTAssertEqual(center.history, [])
|
||||
|
||||
publisher.subscribe(tracking)
|
||||
|
||||
XCTAssertEqual(center.history,
|
||||
[.addObserver(name, TestObject.one, nil)])
|
||||
XCTAssertEqual(tracking.history,
|
||||
[.subscription("NotificationCenter Observer")])
|
||||
|
||||
let note = Notification(name: name, object: TestObject.one, userInfo: nil)
|
||||
center.post(note)
|
||||
|
||||
XCTAssertEqual(center.history,
|
||||
[.addObserver(name, TestObject.one, nil),
|
||||
.postNotification(note)])
|
||||
XCTAssertEqual(tracking.history,
|
||||
[.subscription("NotificationCenter Observer")])
|
||||
|
||||
try XCTUnwrap(downstreamSubscription).request(.max(3))
|
||||
|
||||
center.post(note)
|
||||
|
||||
XCTAssertEqual(center.history,
|
||||
[.addObserver(name, TestObject.one, nil),
|
||||
.postNotification(note),
|
||||
.postNotification(note)])
|
||||
XCTAssertEqual(tracking.history,
|
||||
[.subscription("NotificationCenter Observer"),
|
||||
.value(note)])
|
||||
|
||||
let unrelatedNote1 = Notification(name: Notification.Name("unrelatedNote1"),
|
||||
object: TestObject.one,
|
||||
userInfo: nil)
|
||||
center.post(unrelatedNote1)
|
||||
|
||||
let unrelatedNote2 = Notification(name: name,
|
||||
object: TestObject.two,
|
||||
userInfo: nil)
|
||||
center.post(unrelatedNote2)
|
||||
center.post(name: name, object: nil)
|
||||
center.post(name: name, object: nil)
|
||||
center.post(name: name, object: TestObject.one)
|
||||
center.post(name: name, object: TestObject.one)
|
||||
|
||||
XCTAssertEqual(center.history,
|
||||
[.addObserver(name, TestObject.one, nil),
|
||||
.postNotification(note),
|
||||
.postNotification(note),
|
||||
.postNotification(unrelatedNote1),
|
||||
.postNotification(unrelatedNote2),
|
||||
.postNotificationWithName(name, nil, nil),
|
||||
.postNotification(Notification(name: name)),
|
||||
.postNotificationWithName(name, nil, nil),
|
||||
.postNotification(Notification(name: name)),
|
||||
.postNotificationWithName(name, TestObject.one, nil),
|
||||
.postNotification(Notification(name: name,
|
||||
object: TestObject.one)),
|
||||
.postNotificationWithName(name, TestObject.one, nil),
|
||||
.postNotification(Notification(name: name,
|
||||
object: TestObject.one))])
|
||||
XCTAssertEqual(tracking.history,
|
||||
[.subscription("NotificationCenter Observer"),
|
||||
.value(note),
|
||||
.value(unrelatedNote1),
|
||||
.value(unrelatedNote2)])
|
||||
|
||||
try XCTUnwrap(downstreamSubscription).request(.unlimited)
|
||||
|
||||
center.post(note)
|
||||
|
||||
try XCTUnwrap(downstreamSubscription).cancel()
|
||||
|
||||
center.post(note)
|
||||
|
||||
XCTAssertEqual(center.history,
|
||||
[.addObserver(name, TestObject.one, nil),
|
||||
.postNotification(note),
|
||||
.postNotification(note),
|
||||
.postNotification(unrelatedNote1),
|
||||
.postNotification(unrelatedNote2),
|
||||
.postNotificationWithName(name, nil, nil),
|
||||
.postNotification(Notification(name: name)),
|
||||
.postNotificationWithName(name, nil, nil),
|
||||
.postNotification(Notification(name: name)),
|
||||
.postNotificationWithName(name, TestObject.one, nil),
|
||||
.postNotification(Notification(name: name,
|
||||
object: TestObject.one)),
|
||||
.postNotificationWithName(name, TestObject.one, nil),
|
||||
.postNotification(Notification(name: name,
|
||||
object: TestObject.one)),
|
||||
.postNotification(note),
|
||||
.removeObserver,
|
||||
.removeObserverForName(nil, nil),
|
||||
.postNotification(note)])
|
||||
XCTAssertEqual(tracking.history,
|
||||
[.subscription("NotificationCenter Observer"),
|
||||
.value(note),
|
||||
.value(unrelatedNote1),
|
||||
.value(unrelatedNote2),
|
||||
.value(note)])
|
||||
}
|
||||
|
||||
func testBasicBehaviorNilObject() throws {
|
||||
let center = TestNotificationCenter()
|
||||
let name = Notification.Name(rawValue: "testName")
|
||||
let publisher = makePublisher(center, for: name, object: nil)
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriberBase<Notification, Never>(
|
||||
receiveSubscription: { downstreamSubscription = $0 }
|
||||
)
|
||||
|
||||
XCTAssertEqual(center.history, [])
|
||||
|
||||
publisher.subscribe(tracking)
|
||||
|
||||
XCTAssertEqual(center.history,
|
||||
[.addObserver(name, nil, nil)])
|
||||
XCTAssertEqual(tracking.history,
|
||||
[.subscription("NotificationCenter Observer")])
|
||||
|
||||
let note = Notification(name: name, object: TestObject.one, userInfo: nil)
|
||||
center.post(note)
|
||||
|
||||
XCTAssertEqual(center.history,
|
||||
[.addObserver(name, nil, nil),
|
||||
.postNotification(note)])
|
||||
XCTAssertEqual(tracking.history,
|
||||
[.subscription("NotificationCenter Observer")])
|
||||
|
||||
try XCTUnwrap(downstreamSubscription).request(.max(3))
|
||||
|
||||
center.post(note)
|
||||
|
||||
XCTAssertEqual(center.history,
|
||||
[.addObserver(name, nil, nil),
|
||||
.postNotification(note),
|
||||
.postNotification(note)])
|
||||
XCTAssertEqual(tracking.history,
|
||||
[.subscription("NotificationCenter Observer"),
|
||||
.value(note)])
|
||||
|
||||
let unrelatedNote = Notification(name: Notification.Name("unrelatedNote"),
|
||||
object: TestObject.one,
|
||||
userInfo: nil)
|
||||
center.post(unrelatedNote)
|
||||
center.post(name: name, object: nil)
|
||||
center.post(name: name, object: nil)
|
||||
center.post(name: name, object: TestObject.one)
|
||||
center.post(name: name, object: TestObject.one)
|
||||
|
||||
XCTAssertEqual(center.history,
|
||||
[.addObserver(name, nil, nil),
|
||||
.postNotification(note),
|
||||
.postNotification(note),
|
||||
.postNotification(unrelatedNote),
|
||||
.postNotificationWithName(name, nil, nil),
|
||||
.postNotification(Notification(name: name)),
|
||||
.postNotificationWithName(name, nil, nil),
|
||||
.postNotification(Notification(name: name)),
|
||||
.postNotificationWithName(name, TestObject.one, nil),
|
||||
.postNotification(note),
|
||||
.postNotificationWithName(name, TestObject.one, nil),
|
||||
.postNotification(note)])
|
||||
XCTAssertEqual(tracking.history,
|
||||
[.subscription("NotificationCenter Observer"),
|
||||
.value(note),
|
||||
.value(unrelatedNote),
|
||||
.value(Notification(name: name))])
|
||||
|
||||
try XCTUnwrap(downstreamSubscription).request(.unlimited)
|
||||
|
||||
center.post(note)
|
||||
|
||||
try XCTUnwrap(downstreamSubscription).cancel()
|
||||
|
||||
center.post(note)
|
||||
|
||||
XCTAssertEqual(center.history,
|
||||
[.addObserver(name, nil, nil),
|
||||
.postNotification(note),
|
||||
.postNotification(note),
|
||||
.postNotification(unrelatedNote),
|
||||
.postNotificationWithName(name, nil, nil),
|
||||
.postNotification(Notification(name: name)),
|
||||
.postNotificationWithName(name, nil, nil),
|
||||
.postNotification(Notification(name: name)),
|
||||
.postNotificationWithName(name, TestObject.one, nil),
|
||||
.postNotification(note),
|
||||
.postNotificationWithName(name, TestObject.one, nil),
|
||||
.postNotification(note),
|
||||
.postNotification(note),
|
||||
.removeObserver,
|
||||
.removeObserverForName(nil, nil),
|
||||
.postNotification(note)])
|
||||
XCTAssertEqual(tracking.history,
|
||||
[.subscription("NotificationCenter Observer"),
|
||||
.value(note),
|
||||
.value(unrelatedNote),
|
||||
.value(Notification(name: name)),
|
||||
.value(note)])
|
||||
}
|
||||
|
||||
func testRecursivelyReceiveValue() throws {
|
||||
let center = TestNotificationCenter()
|
||||
let name = Notification.Name(rawValue: "testName")
|
||||
let publisher = makePublisher(center, for: name, object: nil)
|
||||
let tracking = TrackingSubscriberBase<Notification, Never>(
|
||||
receiveSubscription: { $0.request(.max(3)) },
|
||||
receiveValue: { _ in .unlimited }
|
||||
)
|
||||
publisher.subscribe(tracking)
|
||||
|
||||
let note = Notification(name: name)
|
||||
var recursionCounter = 7
|
||||
tracking.onValue = { _ in
|
||||
if recursionCounter == 0 { return }
|
||||
recursionCounter -= 1
|
||||
center.post(note)
|
||||
}
|
||||
|
||||
center.post(note)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("NotificationCenter Observer"),
|
||||
.value(note),
|
||||
.value(note),
|
||||
.value(note)])
|
||||
|
||||
center.post(note)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("NotificationCenter Observer"),
|
||||
.value(note),
|
||||
.value(note),
|
||||
.value(note),
|
||||
.value(note),
|
||||
.value(note),
|
||||
.value(note),
|
||||
.value(note),
|
||||
.value(note)])
|
||||
}
|
||||
|
||||
func testCancelAlreadyCancelled() throws {
|
||||
let center = TestNotificationCenter()
|
||||
let name = Notification.Name(rawValue: "testName")
|
||||
let publisher = makePublisher(center, for: name, object: nil)
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriberBase<Notification, Never>(
|
||||
receiveSubscription: { downstreamSubscription = $0 }
|
||||
)
|
||||
publisher.subscribe(tracking)
|
||||
|
||||
try XCTUnwrap(downstreamSubscription).cancel()
|
||||
try XCTUnwrap(downstreamSubscription).cancel()
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("NotificationCenter Observer")])
|
||||
XCTAssertEqual(center.history,
|
||||
[.addObserver(name, nil, nil),
|
||||
.removeObserver,
|
||||
.removeObserverForName(nil, nil)])
|
||||
}
|
||||
|
||||
func testCancellingReleasesNotificationCenter() throws {
|
||||
var centerDestroyed = false
|
||||
var downstreamSubscription: Subscription?
|
||||
do {
|
||||
let center = TestNotificationCenter()
|
||||
center.onDeinit = { centerDestroyed = true }
|
||||
let name = Notification.Name(rawValue: "testName")
|
||||
let publisher = makePublisher(center, for: name, object: nil)
|
||||
let tracking = TrackingSubscriberBase<Notification, Never>(
|
||||
receiveSubscription: { downstreamSubscription = $0 }
|
||||
)
|
||||
publisher.subscribe(tracking)
|
||||
}
|
||||
XCTAssertFalse(centerDestroyed)
|
||||
try XCTUnwrap(downstreamSubscription).cancel()
|
||||
XCTAssertTrue(centerDestroyed)
|
||||
}
|
||||
|
||||
func testWeakCaptureWhenAddingObserver() {
|
||||
let center = TestNotificationCenter()
|
||||
let name = Notification.Name("testName")
|
||||
var value: Notification?
|
||||
do {
|
||||
let publisher = makePublisher(center, for: name, object: nil)
|
||||
let tracking = TrackingSubscriberBase<Notification, Never>(
|
||||
receiveSubscription: { $0.request(.max(1)) },
|
||||
receiveValue: { value = $0; return .none }
|
||||
)
|
||||
publisher.subscribe(tracking)
|
||||
tracking.clearHistory() // Release the subscription
|
||||
}
|
||||
center.post(name: name, object: nil)
|
||||
XCTAssertEqual(center.history,
|
||||
[.addObserver(name, nil, nil),
|
||||
.postNotificationWithName(name, nil, nil),
|
||||
.postNotification(Notification(name: name))])
|
||||
XCTAssertNil(value)
|
||||
}
|
||||
|
||||
func testZeroDemand() throws {
|
||||
let center = TestNotificationCenter()
|
||||
let name = Notification.Name(rawValue: "testName")
|
||||
let publisher = makePublisher(center, for: name, object: nil)
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriberBase<Notification, Never>(
|
||||
receiveSubscription: { downstreamSubscription = $0 }
|
||||
)
|
||||
publisher.subscribe(tracking)
|
||||
try XCTUnwrap(downstreamSubscription).request(.none)
|
||||
|
||||
XCTAssertEqual(center.history,
|
||||
[.addObserver(name, nil, nil)])
|
||||
XCTAssertEqual(tracking.history, [.subscription("NotificationCenter Observer")])
|
||||
}
|
||||
|
||||
func testNotificationCenterSubscriptionReflection() throws {
|
||||
let center = TestNotificationCenter()
|
||||
let name = Notification.Name(rawValue: "testName")
|
||||
let object = TestObject.one
|
||||
let publisher = makePublisher(center, for: name, object: object)
|
||||
|
||||
try testSubscriptionReflection(
|
||||
description: "NotificationCenter Observer",
|
||||
customMirror: expectedChildren(
|
||||
("center", .matches(String(describing: Optional(center)))),
|
||||
("name", .contains(String(describing: name))),
|
||||
("object", .matches(String(describing: Optional(object)))),
|
||||
("demand", "max(0)")
|
||||
),
|
||||
playgroundDescription: "NotificationCenter Observer",
|
||||
sut: publisher
|
||||
)
|
||||
}
|
||||
|
||||
func testEquatable() {
|
||||
let center1 = NotificationCenter()
|
||||
let center2 = NotificationCenter()
|
||||
let name1 = Notification.Name(rawValue: "abcdefg")
|
||||
let name2 = Notification.Name(rawValue: "1234567")
|
||||
let object1 = TestObject.one
|
||||
let object2 = TestObject.two
|
||||
|
||||
XCTAssertEqual(makePublisher(center1, for: name1, object: object1),
|
||||
makePublisher(center1, for: name1, object: object1))
|
||||
XCTAssertEqual(makePublisher(center2, for: name2, object: object2),
|
||||
makePublisher(center2, for: name2, object: object2))
|
||||
XCTAssertEqual(makePublisher(center1, for: name1, object: nil),
|
||||
makePublisher(center1, for: name1, object: nil))
|
||||
XCTAssertNotEqual(makePublisher(center1, for: name1, object: object1),
|
||||
makePublisher(center1, for: name1, object: nil))
|
||||
XCTAssertNotEqual(makePublisher(center1, for: name1, object: nil),
|
||||
makePublisher(center1, for: name1, object: object2))
|
||||
XCTAssertNotEqual(makePublisher(center1, for: name1, object: object1),
|
||||
makePublisher(center1, for: name1, object: object2))
|
||||
XCTAssertNotEqual(makePublisher(center1, for: name1, object: object1),
|
||||
makePublisher(center1, for: name2, object: object1))
|
||||
XCTAssertNotEqual(makePublisher(center1, for: name1, object: object1),
|
||||
makePublisher(center2, for: name1, object: object1))
|
||||
}
|
||||
}
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST || !canImport(Combine)
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private func makePublisher(
|
||||
_ center: NotificationCenter,
|
||||
for name: Notification.Name,
|
||||
object: AnyObject?
|
||||
) -> NotificationCenter.Publisher {
|
||||
return center.publisher(for: name, object: object)
|
||||
}
|
||||
#else
|
||||
private func makePublisher(
|
||||
_ center: NotificationCenter,
|
||||
for name: Notification.Name,
|
||||
object: AnyObject?
|
||||
) -> NotificationCenter.OCombine.Publisher {
|
||||
return center.ocombine.publisher(for: name, object: object)
|
||||
}
|
||||
#endif
|
||||
|
||||
/// A simple mock notification center that always sends notifications to **all**
|
||||
/// observers in non-thread safe manner.
|
||||
private final class TestNotificationCenter: NotificationCenter {
|
||||
|
||||
enum Event {
|
||||
case postNotificationWithName(Notification.Name, Any?, [AnyHashable : Any]?)
|
||||
case postNotification(Notification)
|
||||
case addObserver(Notification.Name?, Any?, OperationQueue?)
|
||||
case removeObserver
|
||||
case removeObserverForName(Notification.Name?, Any?)
|
||||
}
|
||||
|
||||
private final class Observation {
|
||||
let callback: (Notification) -> Void
|
||||
|
||||
init(callback: @escaping (Notification) -> Void) {
|
||||
self.callback = callback
|
||||
}
|
||||
}
|
||||
|
||||
private final class Token: NSObject {
|
||||
weak var observation: Observation?
|
||||
|
||||
init(observer: TestNotificationCenter.Observation) {
|
||||
self.observation = observer
|
||||
}
|
||||
}
|
||||
|
||||
private(set) var history = [Event]()
|
||||
|
||||
private var observations: [Observation] = []
|
||||
|
||||
var onDeinit: (() -> Void)?
|
||||
|
||||
deinit {
|
||||
onDeinit?()
|
||||
}
|
||||
|
||||
override func post(name aName: Notification.Name,
|
||||
object anObject: Any?,
|
||||
userInfo aUserInfo: [AnyHashable : Any]? = nil) {
|
||||
history.append(.postNotificationWithName(aName, anObject, aUserInfo))
|
||||
let notification = Notification(name: aName,
|
||||
object: anObject,
|
||||
userInfo: aUserInfo)
|
||||
post(notification)
|
||||
}
|
||||
|
||||
override func post(_ notification: Notification) {
|
||||
history.append(.postNotification(notification))
|
||||
for observation in observations {
|
||||
observation.callback(notification)
|
||||
}
|
||||
}
|
||||
|
||||
override func addObserver(
|
||||
forName name: NSNotification.Name?,
|
||||
object obj: Any?,
|
||||
queue: OperationQueue?,
|
||||
using block: @escaping (Notification) -> Void
|
||||
) -> NSObjectProtocol {
|
||||
history.append(.addObserver(name, obj, queue))
|
||||
let observer = Observation(callback: block)
|
||||
observations.append(observer)
|
||||
return Token(observer: observer)
|
||||
}
|
||||
|
||||
override func removeObserver(_ observer: Any) {
|
||||
history.append(.removeObserver)
|
||||
removeObserver(observer, name: nil, object: nil)
|
||||
}
|
||||
|
||||
override func removeObserver(_ observer: Any,
|
||||
name aName: NSNotification.Name?,
|
||||
object anObject: Any?) {
|
||||
history.append(.removeObserverForName(aName, anObject))
|
||||
guard let observer = observer as? Token else { return }
|
||||
observations.removeAll { $0 === observer.observation }
|
||||
}
|
||||
}
|
||||
|
||||
private final class TestObject: NSObject {
|
||||
|
||||
static let one = TestObject()
|
||||
|
||||
static let two = TestObject()
|
||||
}
|
||||
|
||||
extension TestNotificationCenter.Event: Equatable {
|
||||
fileprivate static func == (lhs: TestNotificationCenter.Event,
|
||||
rhs: TestNotificationCenter.Event) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case let (.postNotification(lhsNote), .postNotification(rhsNote)):
|
||||
return lhsNote == rhsNote
|
||||
case let (.postNotificationWithName(lhsName,
|
||||
lhsObject as TestObject?,
|
||||
lhsUserInfo),
|
||||
.postNotificationWithName(rhsName,
|
||||
rhsObject as TestObject?,
|
||||
rhsUserInfo)):
|
||||
return lhsName == rhsName &&
|
||||
lhsObject === rhsObject &&
|
||||
(lhsUserInfo == nil) == (rhsUserInfo == nil)
|
||||
case let (.addObserver(lhsName, lhsObject as TestObject?, lhsQueue),
|
||||
.addObserver(rhsName, rhsObject as TestObject?, rhsQueue)):
|
||||
return lhsName == rhsName &&
|
||||
lhsObject === rhsObject &&
|
||||
lhsQueue == rhsQueue
|
||||
case (.removeObserver, .removeObserver):
|
||||
return true
|
||||
case let (.removeObserverForName(lhsName, lhsObject as TestObject?),
|
||||
.removeObserverForName(rhsName, rhsObject as TestObject?)):
|
||||
return lhsName == rhsName && lhsObject === rhsObject
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension TestNotificationCenter.Event: CustomStringConvertible {
|
||||
var description: String {
|
||||
switch self {
|
||||
case let .postNotificationWithName(name, object, userInfo):
|
||||
return """
|
||||
.postNotificationWithName(\
|
||||
.init(rawValue: \"\(name.rawValue)\"), \
|
||||
\(object.map(String.init(describing:)) ?? "nil"), \
|
||||
\(userInfo.map(String.init(describing:)) ?? "nil"))
|
||||
"""
|
||||
case .postNotification:
|
||||
return ".postNotification(note)"
|
||||
case let .addObserver(name, object, queue):
|
||||
let nameDescription = name.map { ".init(rawValue: \($0.rawValue))" } ?? "nil"
|
||||
return """
|
||||
.addObserver(\
|
||||
\(nameDescription), \
|
||||
\(object.map(String.init(describing:)) ?? "nil"), \
|
||||
\(queue.map(String.init(describing:)) ?? "nil"))
|
||||
"""
|
||||
case .removeObserver:
|
||||
return ".removeObserver"
|
||||
case let .removeObserverForName(name, object):
|
||||
let nameDescription = name.map { ".init(rawValue: \($0.rawValue))" } ?? "nil"
|
||||
return """
|
||||
.removeObserverForName(\
|
||||
\(nameDescription), \
|
||||
\(object.map(String.init(describing:)) ?? "nil"))
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
//
|
||||
// PropertyListDecoderTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 10.12.2019.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
import OpenCombineFoundation
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class PropertyListDecoderTests: XCTestCase {
|
||||
func testSuccessfullyDecode() {
|
||||
let decoder = PropertyListDecoder()
|
||||
let input = """
|
||||
<?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">
|
||||
<array>
|
||||
\t<dict>
|
||||
\t\t<key>success</key>
|
||||
\t\t<true/>
|
||||
\t</dict>
|
||||
</array>
|
||||
</plist>
|
||||
|
||||
"""
|
||||
var actualOutput: [Subscribers.Completion<TestingError>]?
|
||||
var actualCompletion: Subscribers.Completion<Error>?
|
||||
let cancellable = Just(input)
|
||||
.map { Data($0.utf8) }
|
||||
.decode(type: [Subscribers.Completion<TestingError>].self, decoder: decoder)
|
||||
.sink(receiveCompletion: { actualCompletion = $0 },
|
||||
receiveValue: { actualOutput = $0 })
|
||||
switch actualCompletion {
|
||||
case .finished?:
|
||||
XCTAssertEqual(actualOutput, [.finished])
|
||||
case .failure(let error)?:
|
||||
XCTFail("Unexpected failure received: \(error)")
|
||||
case nil:
|
||||
XCTFail("Expected completion")
|
||||
}
|
||||
cancellable.cancel()
|
||||
}
|
||||
|
||||
func testDecodingFailure() {
|
||||
let decoder = PropertyListDecoder()
|
||||
let input = "000000"
|
||||
var actualOutput: [Int]?
|
||||
var actualCompletion: Subscribers.Completion<Error>?
|
||||
let cancellable = Just(input)
|
||||
.map { Data($0.utf8) }
|
||||
.decode(type: [Int].self, decoder: decoder)
|
||||
.sink(receiveCompletion: { actualCompletion = $0 },
|
||||
receiveValue: { actualOutput = $0 })
|
||||
switch actualCompletion {
|
||||
case .finished?:
|
||||
XCTFail("Unexpected success")
|
||||
case .failure(DecodingError.typeMismatch)?:
|
||||
XCTAssertNil(actualOutput)
|
||||
case .failure(let error)?:
|
||||
XCTFail("Unexpected failure received: \(error)")
|
||||
case nil:
|
||||
XCTFail("Expected completion")
|
||||
}
|
||||
cancellable.cancel()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
//
|
||||
// PropertyListEncoderTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 10.12.2019.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
import OpenCombineFoundation
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class PropertyListEncoderTests: XCTestCase {
|
||||
|
||||
func testSuccessfullyEncode() {
|
||||
let encoder = PropertyListEncoder()
|
||||
encoder.outputFormat = .xml
|
||||
let input = [Subscribers.Completion<TestingError>.finished]
|
||||
var actualOutput: String?
|
||||
var actualCompletion: Subscribers.Completion<Error>?
|
||||
|
||||
let cancellable = Just(input)
|
||||
.encode(encoder: encoder)
|
||||
.map { String(decoding: $0, as: UTF8.self) }
|
||||
.sink(receiveCompletion: { actualCompletion = $0 },
|
||||
receiveValue: { actualOutput = $0 })
|
||||
|
||||
switch actualCompletion {
|
||||
case .finished?:
|
||||
XCTAssertEqual(actualOutput, """
|
||||
<?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">
|
||||
<array>
|
||||
\t<dict>
|
||||
\t\t<key>success</key>
|
||||
\t\t<true/>
|
||||
\t</dict>
|
||||
</array>
|
||||
</plist>
|
||||
|
||||
""")
|
||||
case .failure(let error)?:
|
||||
XCTFail("Unexpected failure received: \(error)")
|
||||
case nil:
|
||||
XCTFail("Expected completion")
|
||||
}
|
||||
cancellable.cancel()
|
||||
}
|
||||
|
||||
func testEncodingFailure() {
|
||||
let encoder = PropertyListEncoder()
|
||||
let input = Double.nan
|
||||
var actualOutput: String?
|
||||
var actualCompletion: Subscribers.Completion<Error>?
|
||||
let cancellable = Just(input)
|
||||
.encode(encoder: encoder)
|
||||
.map { String(decoding: $0, as: UTF8.self) }
|
||||
.sink(receiveCompletion: { actualCompletion = $0 },
|
||||
receiveValue: { actualOutput = $0 })
|
||||
|
||||
switch actualCompletion {
|
||||
case .finished?:
|
||||
XCTFail("Unexpected success")
|
||||
case .failure(EncodingError.invalidValue)?:
|
||||
XCTAssertNil(actualOutput)
|
||||
case .failure(let error)?:
|
||||
XCTFail("Unexpected failure received: \(error)")
|
||||
case nil:
|
||||
XCTFail("Expected completion")
|
||||
}
|
||||
cancellable.cancel()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,724 @@
|
||||
//
|
||||
// URLSessionTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 13.12.2019.
|
||||
//
|
||||
|
||||
// swiftlint:disable multiline_arguments
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
#if canImport(FoundationNetworking)
|
||||
import FoundationNetworking
|
||||
#endif
|
||||
|
||||
// We can't test it on non-Darwin platforms because swift-corelibs-foundation
|
||||
// doesn't allow us to override some URLSession methods that we need.
|
||||
//
|
||||
// As soon as https://github.com/apple/swift-corelibs-foundation/pull/2587 makes it
|
||||
// into a release, we can enable these tests on non-Darwin platforms.
|
||||
//
|
||||
// The publisher itself though should work alright on those platforms.
|
||||
#if canImport(Darwin)
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
import OpenCombineFoundation
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class URLSessionTests: XCTestCase {
|
||||
|
||||
private typealias TrackingSubscriber =
|
||||
TrackingSubscriberBase<(data: Data, response: URLResponse), URLError>
|
||||
|
||||
private let testURL = URL(string: "https://github.com")!
|
||||
|
||||
private let testRequest = URLRequest(url: URL(string: "https://github.com")!,
|
||||
cachePolicy: .reloadIgnoringCacheData,
|
||||
timeoutInterval: 42)
|
||||
|
||||
private let testData = Data("test data".utf8)
|
||||
|
||||
private let testResponse = URLResponse(url: URL(string: "https://example.com")!,
|
||||
mimeType: "text/markdown",
|
||||
expectedContentLength: 300,
|
||||
textEncodingName: "utf-8")
|
||||
|
||||
private let testError = URLError(.cannotParseResponse, userInfo: ["a" : 1])
|
||||
|
||||
private let unknownError = URLError(.unknown)
|
||||
|
||||
func testDataTaskPublisherFromURL() {
|
||||
let publisher = makePublisher(TestURLSession(testDataTask: .init()), testURL)
|
||||
let expectedRequest = URLRequest(url: testURL)
|
||||
XCTAssertEqual(publisher.request, expectedRequest)
|
||||
}
|
||||
|
||||
func testDataTaskPublisherFromRequest() {
|
||||
let publisher = makePublisher(TestURLSession(testDataTask: .init()), testRequest)
|
||||
XCTAssertEqual(publisher.request, testRequest)
|
||||
}
|
||||
|
||||
func testReceiveNothing() {
|
||||
testReceiveResult(nil, nil, nil,
|
||||
expected: [.subscription("DataTaskPublisher"),
|
||||
.completion(.failure(unknownError))])
|
||||
}
|
||||
|
||||
func testReceiveOnlyData() {
|
||||
testReceiveResult(testData, nil, nil,
|
||||
expected: [.subscription("DataTaskPublisher"),
|
||||
.completion(.failure(unknownError))])
|
||||
}
|
||||
|
||||
func testReceiveDataAndResponse() {
|
||||
testReceiveResult(testData, testResponse, nil,
|
||||
expected: [.subscription("DataTaskPublisher"),
|
||||
.value((testData, testResponse)),
|
||||
.completion(.finished)])
|
||||
}
|
||||
|
||||
func testReceiveDataAndURLError() {
|
||||
testReceiveResult(testData, nil, testError,
|
||||
expected: [.subscription("DataTaskPublisher"),
|
||||
.completion(.failure(testError))])
|
||||
}
|
||||
|
||||
func testReceiveDataAndUnrelatedError() {
|
||||
testReceiveResult(testData, nil, TestingError.oops,
|
||||
expected: [.subscription("DataTaskPublisher"),
|
||||
.completion(.failure(unknownError))])
|
||||
}
|
||||
|
||||
func testReceiveOnlyResponse() {
|
||||
testReceiveResult(nil, testResponse, nil,
|
||||
expected: [.subscription("DataTaskPublisher"),
|
||||
.value((Data(), testResponse)),
|
||||
.completion(.finished)])
|
||||
}
|
||||
|
||||
func testReceiveResponseAndURLError() {
|
||||
testReceiveResult(nil, testResponse, testError,
|
||||
expected: [.subscription("DataTaskPublisher"),
|
||||
.completion(.failure(testError))])
|
||||
}
|
||||
|
||||
func testReceiveResponseAndUnrelatedError() {
|
||||
testReceiveResult(nil, testResponse, TestingError.oops,
|
||||
expected: [.subscription("DataTaskPublisher"),
|
||||
.completion(.failure(unknownError))])
|
||||
}
|
||||
|
||||
func testReceiveOnlyURLError() {
|
||||
testReceiveResult(nil, nil, testError,
|
||||
expected: [.subscription("DataTaskPublisher"),
|
||||
.completion(.failure(testError))])
|
||||
}
|
||||
|
||||
func testReceiveOnlyUnrelatedError() {
|
||||
testReceiveResult(nil, nil, TestingError.oops,
|
||||
expected: [.subscription("DataTaskPublisher"),
|
||||
.completion(.failure(unknownError))])
|
||||
}
|
||||
|
||||
func testRequesting() throws {
|
||||
let dataTask = TestURLSessionDataTask()
|
||||
let session = TestURLSession(testDataTask: dataTask)
|
||||
let publisher = makePublisher(session, testRequest)
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: { downstreamSubscription = $0 }
|
||||
)
|
||||
publisher.subscribe(tracking)
|
||||
|
||||
tracking.assertHistoryEqual([.subscription("DataTaskPublisher")],
|
||||
valueComparator: ==)
|
||||
XCTAssertEqual(dataTask.history, [])
|
||||
XCTAssertEqual(session.history, [])
|
||||
|
||||
session.completeDataTasks(testData, testResponse, nil)
|
||||
|
||||
try XCTUnwrap(downstreamSubscription).request(.max(2))
|
||||
try XCTUnwrap(downstreamSubscription).request(.max(1))
|
||||
|
||||
tracking.assertHistoryEqual([.subscription("DataTaskPublisher")],
|
||||
valueComparator: ==)
|
||||
XCTAssertEqual(dataTask.history, [.resume, .resume])
|
||||
XCTAssertEqual(session.history, [.dataTaskWithRequestAndCompletion(testRequest)])
|
||||
|
||||
try XCTUnwrap(downstreamSubscription).cancel()
|
||||
|
||||
tracking.assertHistoryEqual([.subscription("DataTaskPublisher")],
|
||||
valueComparator: ==)
|
||||
XCTAssertEqual(dataTask.history, [.resume, .resume, .cancel])
|
||||
XCTAssertEqual(session.history, [.dataTaskWithRequestAndCompletion(testRequest)])
|
||||
|
||||
session.completeDataTasks(testData, testResponse, nil)
|
||||
|
||||
tracking.assertHistoryEqual([.subscription("DataTaskPublisher")],
|
||||
valueComparator: ==)
|
||||
XCTAssertEqual(dataTask.history, [.resume, .resume, .cancel])
|
||||
XCTAssertEqual(session.history, [.dataTaskWithRequestAndCompletion(testRequest)])
|
||||
}
|
||||
|
||||
func testCancelAlreadyCancelled() throws {
|
||||
let dataTask = TestURLSessionDataTask()
|
||||
let session = TestURLSession(testDataTask: dataTask)
|
||||
let publisher = makePublisher(session, testRequest)
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: { downstreamSubscription = $0 }
|
||||
)
|
||||
publisher.subscribe(tracking)
|
||||
|
||||
tracking.assertHistoryEqual([.subscription("DataTaskPublisher")],
|
||||
valueComparator: ==)
|
||||
XCTAssertEqual(dataTask.history, [])
|
||||
XCTAssertEqual(session.history, [])
|
||||
|
||||
try XCTUnwrap(downstreamSubscription).request(.max(1))
|
||||
try XCTUnwrap(downstreamSubscription).cancel()
|
||||
try XCTUnwrap(downstreamSubscription).request(.max(1))
|
||||
try XCTUnwrap(downstreamSubscription).cancel()
|
||||
|
||||
tracking.assertHistoryEqual([.subscription("DataTaskPublisher")],
|
||||
valueComparator: ==)
|
||||
XCTAssertEqual(dataTask.history, [.resume, .cancel])
|
||||
XCTAssertEqual(session.history, [.dataTaskWithRequestAndCompletion(testRequest)])
|
||||
}
|
||||
|
||||
func testCrashesOnZeroDemand() throws {
|
||||
let dataTask = TestURLSessionDataTask()
|
||||
let session = TestURLSession(testDataTask: dataTask)
|
||||
let publisher = makePublisher(session, testURL)
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: { downstreamSubscription = $0 }
|
||||
)
|
||||
publisher.subscribe(tracking)
|
||||
|
||||
try assertCrashes {
|
||||
try XCTUnwrap(downstreamSubscription).request(.none)
|
||||
}
|
||||
}
|
||||
|
||||
func testURLSessionSubscriptionReflection() throws {
|
||||
let dataTask = TestURLSessionDataTask()
|
||||
let session = TestURLSession(testDataTask: dataTask)
|
||||
let publisher = makePublisher(session, testURL)
|
||||
try testSubscriptionReflection(
|
||||
description: "DataTaskPublisher",
|
||||
customMirror: expectedChildren(
|
||||
("task", "nil"),
|
||||
("downstream", .contains("TrackingSubscriberBase")),
|
||||
("parent", .matches(String(describing: Optional(publisher)))),
|
||||
("demand", "max(0)")
|
||||
),
|
||||
playgroundDescription: "DataTaskPublisher",
|
||||
sut: publisher
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Generic tests
|
||||
|
||||
private func testReceiveResult(_ data: Data?,
|
||||
_ response: URLResponse?,
|
||||
_ error: Error?,
|
||||
expected: [TrackingSubscriber.Event]) {
|
||||
let dataTask = TestURLSessionDataTask()
|
||||
let session = TestURLSession(testDataTask: dataTask)
|
||||
let publisher = makePublisher(session, testRequest)
|
||||
let tracking = TrackingSubscriber(receiveSubscription: { $0.request(.max(1)) })
|
||||
publisher.subscribe(tracking)
|
||||
|
||||
tracking.assertHistoryEqual([.subscription("DataTaskPublisher")],
|
||||
valueComparator: ==)
|
||||
XCTAssertEqual(dataTask.history, [.resume])
|
||||
XCTAssertEqual(session.history, [.dataTaskWithRequestAndCompletion(testRequest)])
|
||||
|
||||
session.completeDataTasks(data, response, error)
|
||||
session.completeDataTasks(data, response, error)
|
||||
session.completeDataTasks(data, response, error)
|
||||
|
||||
tracking.assertHistoryEqual(expected, valueComparator: ==)
|
||||
XCTAssertEqual(dataTask.history, [.resume])
|
||||
XCTAssertEqual(session.history, [.dataTaskWithRequestAndCompletion(testRequest)])
|
||||
}
|
||||
}
|
||||
|
||||
/// A simple mock URLSession that records its history and allows executing
|
||||
/// callbacks synchronously
|
||||
private class TestURLSession: URLSession {
|
||||
|
||||
enum Event: Equatable {
|
||||
case delegateQueue
|
||||
case delegate
|
||||
case configuration
|
||||
case getSessionDescription
|
||||
case setSessionDescription(String?)
|
||||
case finishTasksAndInvalidate
|
||||
case invalidateAndCancel
|
||||
case reset
|
||||
case flush
|
||||
case getTasksWithCompletionHandler
|
||||
case getAllTasks
|
||||
case dataTaskWithRequest(URLRequest)
|
||||
case dataTaskWithRequestAndCompletion(URLRequest)
|
||||
case dataTaskWithURL(URL)
|
||||
case dataTaskWithURLAndCompletion(URL)
|
||||
case uploadTaskWithRequestFromFile(URLRequest, URL)
|
||||
case uploadTaskWithRequestFromFileWithCompletion(URLRequest, URL)
|
||||
case uploadTaskWithRequestFromData(URLRequest, Data)
|
||||
case uploadTaskWithRequestFromDataWithCompletion(URLRequest, Data?)
|
||||
case uploadTaskWithStreamedRequest(URLRequest)
|
||||
case downloadTaskWithRequest(URLRequest)
|
||||
case downloadTaskWithRequestAndCompletion(URLRequest)
|
||||
case downloadTaskWithURL(URL)
|
||||
case downloadTaskWithURLAndCompletion(URL)
|
||||
case downloadTaskWithResumeData(Data)
|
||||
case downloadTaskWithResumeDataAndCompletion(Data)
|
||||
case streamTaskWithHostNameAndPort(String, Int)
|
||||
#if canImport(Darwin) && swift(>=5.1)
|
||||
case streamTaskWithService(NetService)
|
||||
case webSocketTaskWithURL(URL)
|
||||
case webSocketTaskWithURLAndProtocols(URL, [String])
|
||||
case webSocketTaskWithRequest(URLRequest)
|
||||
#endif // canImport(Darwin) && swift(>=5.1)
|
||||
}
|
||||
|
||||
private(set) var history = [Event]()
|
||||
|
||||
private(set) var dataTaskCompletionHandlers: [(Data?, URLResponse?, Error?) -> Void]
|
||||
|
||||
private let testDataTask: TestURLSessionDataTask
|
||||
|
||||
init(testDataTask: TestURLSessionDataTask) {
|
||||
self.testDataTask = testDataTask
|
||||
self.dataTaskCompletionHandlers = []
|
||||
}
|
||||
|
||||
// MARK: Testing
|
||||
|
||||
func completeDataTasks(_ data: Data?, _ response: URLResponse?, _ error: Error?) {
|
||||
for completionHandler in dataTaskCompletionHandlers {
|
||||
completionHandler(data, response, error)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Overrides
|
||||
|
||||
override class var shared: URLSession { fatalError("shared session is unavailable") }
|
||||
|
||||
override var delegateQueue: OperationQueue {
|
||||
history.append(.delegateQueue)
|
||||
return super.delegateQueue
|
||||
}
|
||||
|
||||
override var delegate: URLSessionDelegate? {
|
||||
history.append(.delegate)
|
||||
return super.delegate
|
||||
}
|
||||
|
||||
override var configuration: URLSessionConfiguration {
|
||||
history.append(.configuration)
|
||||
return super.configuration
|
||||
}
|
||||
|
||||
override var sessionDescription: String? {
|
||||
get {
|
||||
history.append(.getSessionDescription)
|
||||
return super.sessionDescription
|
||||
}
|
||||
set {
|
||||
history.append(.setSessionDescription(newValue))
|
||||
super.sessionDescription = newValue
|
||||
}
|
||||
}
|
||||
|
||||
override func finishTasksAndInvalidate() {
|
||||
history.append(.finishTasksAndInvalidate)
|
||||
super.finishTasksAndInvalidate()
|
||||
}
|
||||
|
||||
override func invalidateAndCancel() {
|
||||
history.append(.invalidateAndCancel)
|
||||
super.invalidateAndCancel()
|
||||
}
|
||||
|
||||
override func reset(completionHandler: @escaping () -> Void) {
|
||||
history.append(.reset)
|
||||
super.reset(completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
override func flush(completionHandler: @escaping () -> Void) {
|
||||
history.append(.flush)
|
||||
super.flush(completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
override func getTasksWithCompletionHandler(
|
||||
_ completionHandler: @escaping ([URLSessionDataTask],
|
||||
[URLSessionUploadTask],
|
||||
[URLSessionDownloadTask]) -> Void
|
||||
) {
|
||||
history.append(.getTasksWithCompletionHandler)
|
||||
super.getTasksWithCompletionHandler(completionHandler)
|
||||
}
|
||||
|
||||
@available(macOS 10.11, iOS 9.0, *)
|
||||
override func getAllTasks(completionHandler: @escaping ([URLSessionTask]) -> Void) {
|
||||
history.append(.getAllTasks)
|
||||
super.getAllTasks(completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
override func dataTask(with request: URLRequest) -> URLSessionDataTask {
|
||||
history.append(.dataTaskWithRequest(request))
|
||||
return testDataTask
|
||||
}
|
||||
|
||||
override func dataTask(with url: URL) -> URLSessionDataTask {
|
||||
history.append(.dataTaskWithURL(url))
|
||||
return testDataTask
|
||||
}
|
||||
|
||||
override func dataTask(
|
||||
with url: URL,
|
||||
completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void
|
||||
) -> URLSessionDataTask {
|
||||
history.append(.dataTaskWithURLAndCompletion(url))
|
||||
dataTaskCompletionHandlers.append(completionHandler)
|
||||
return testDataTask
|
||||
}
|
||||
|
||||
override func dataTask(
|
||||
with request: URLRequest,
|
||||
completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void
|
||||
) -> URLSessionDataTask {
|
||||
history.append(.dataTaskWithRequestAndCompletion(request))
|
||||
dataTaskCompletionHandlers.append(completionHandler)
|
||||
return testDataTask
|
||||
}
|
||||
|
||||
override func uploadTask(with request: URLRequest,
|
||||
fromFile fileURL: URL) -> URLSessionUploadTask {
|
||||
history.append(.uploadTaskWithRequestFromFile(request, fileURL))
|
||||
return super.uploadTask(with: request, fromFile: fileURL)
|
||||
}
|
||||
|
||||
override func uploadTask(with request: URLRequest,
|
||||
from bodyData: Data) -> URLSessionUploadTask {
|
||||
history.append(.uploadTaskWithRequestFromData(request, bodyData))
|
||||
return super.uploadTask(with: request, from: bodyData)
|
||||
}
|
||||
|
||||
override func uploadTask(
|
||||
withStreamedRequest request: URLRequest
|
||||
) -> URLSessionUploadTask {
|
||||
history.append(.uploadTaskWithStreamedRequest(request))
|
||||
return super.uploadTask(withStreamedRequest: request)
|
||||
}
|
||||
|
||||
override func uploadTask(
|
||||
with request: URLRequest,
|
||||
fromFile fileURL: URL,
|
||||
completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void
|
||||
) -> URLSessionUploadTask {
|
||||
history.append(.uploadTaskWithRequestFromFileWithCompletion(request, fileURL))
|
||||
return super.uploadTask(with: request,
|
||||
fromFile: fileURL,
|
||||
completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
override func uploadTask(
|
||||
with request: URLRequest,
|
||||
from bodyData: Data?,
|
||||
completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void
|
||||
) -> URLSessionUploadTask {
|
||||
history.append(.uploadTaskWithRequestFromDataWithCompletion(request, bodyData))
|
||||
return super.uploadTask(with: request,
|
||||
from: bodyData,
|
||||
completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
override func downloadTask(with request: URLRequest) -> URLSessionDownloadTask {
|
||||
history.append(.downloadTaskWithRequest(request))
|
||||
return super.downloadTask(with: request)
|
||||
}
|
||||
|
||||
override func downloadTask(with url: URL) -> URLSessionDownloadTask {
|
||||
history.append(.downloadTaskWithURL(url))
|
||||
return super.downloadTask(with: url)
|
||||
}
|
||||
|
||||
override func downloadTask(
|
||||
with request: URLRequest,
|
||||
completionHandler: @escaping (URL?, URLResponse?, Error?) -> Void
|
||||
) -> URLSessionDownloadTask {
|
||||
history.append(.downloadTaskWithRequestAndCompletion(request))
|
||||
return super.downloadTask(with: request, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
override func downloadTask(
|
||||
with url: URL,
|
||||
completionHandler: @escaping (URL?, URLResponse?, Error?) -> Void
|
||||
) -> URLSessionDownloadTask {
|
||||
history.append(.downloadTaskWithURLAndCompletion(url))
|
||||
return super.downloadTask(with: url, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
override func downloadTask(
|
||||
withResumeData resumeData: Data,
|
||||
completionHandler: @escaping (URL?, URLResponse?, Error?) -> Void
|
||||
) -> URLSessionDownloadTask {
|
||||
history.append(.downloadTaskWithResumeDataAndCompletion(resumeData))
|
||||
return super.downloadTask(withResumeData: resumeData,
|
||||
completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
override func downloadTask(
|
||||
withResumeData resumeData: Data
|
||||
) -> URLSessionDownloadTask {
|
||||
history.append(.downloadTaskWithResumeData(resumeData))
|
||||
return super.downloadTask(withResumeData: resumeData)
|
||||
}
|
||||
|
||||
@available(macOS 10.11, iOS 9.0, *)
|
||||
override func streamTask(withHostName hostname: String,
|
||||
port: Int) -> URLSessionStreamTask {
|
||||
history.append(.streamTaskWithHostNameAndPort(hostname, port))
|
||||
return super.streamTask(withHostName: hostname, port: port)
|
||||
}
|
||||
|
||||
#if canImport(Darwin) && swift(>=5.1)
|
||||
@available(macOS 10.11, iOS 9.0, *)
|
||||
override func streamTask(with service: NetService) -> URLSessionStreamTask {
|
||||
history.append(.streamTaskWithService(service))
|
||||
return super.streamTask(with: service)
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
override func webSocketTask(with url: URL) -> URLSessionWebSocketTask {
|
||||
history.append(.webSocketTaskWithURL(url))
|
||||
return super.webSocketTask(with: url)
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
override func webSocketTask(with url: URL,
|
||||
protocols: [String]) -> URLSessionWebSocketTask {
|
||||
history.append(.webSocketTaskWithURLAndProtocols(url, protocols))
|
||||
return super.webSocketTask(with: url, protocols: protocols)
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
override func webSocketTask(with request: URLRequest) -> URLSessionWebSocketTask {
|
||||
history.append(.webSocketTaskWithRequest(request))
|
||||
return super.webSocketTask(with: request)
|
||||
}
|
||||
#endif // canImport(Darwin) && swift(>=5.1)
|
||||
}
|
||||
|
||||
private final class TestURLSessionDataTask: URLSessionDataTask {
|
||||
|
||||
enum Event: Equatable {
|
||||
case taskIdentifier
|
||||
case originalRequest
|
||||
case currentRequest
|
||||
case response
|
||||
case progress
|
||||
case getEarliestBeginDate
|
||||
case setEarliestBeginDate(Date?)
|
||||
case getCountOfBytesClientExpectsToSend
|
||||
case setCountOfBytesClientExpectsToSend(Int64)
|
||||
case getCountOfBytesClientExpectsToReceive
|
||||
case setCountOfBytesClientExpectsToReceive(Int64)
|
||||
case countOfBytesReceived
|
||||
case countOfBytesSent
|
||||
case countOfBytesExpectedToSend
|
||||
case countOfBytesExpectedToReceive
|
||||
case getTaskDescription
|
||||
case setTaskDescription(String?)
|
||||
case cancel
|
||||
case state
|
||||
case error
|
||||
case suspend
|
||||
case resume
|
||||
case getPriority
|
||||
case setPriority(Float)
|
||||
}
|
||||
|
||||
private(set) var history = [Event]()
|
||||
|
||||
override init() {}
|
||||
|
||||
override var taskIdentifier: Int {
|
||||
history.append(.taskIdentifier)
|
||||
return super.taskIdentifier
|
||||
}
|
||||
|
||||
override var originalRequest: URLRequest? {
|
||||
history.append(.originalRequest)
|
||||
return super.originalRequest
|
||||
}
|
||||
|
||||
override var currentRequest: URLRequest? {
|
||||
history.append(.currentRequest)
|
||||
return super.currentRequest
|
||||
}
|
||||
|
||||
override var response: URLResponse? {
|
||||
history.append(.response)
|
||||
return super.response
|
||||
}
|
||||
|
||||
@available(macOS 10.13, iOS 11.0, *)
|
||||
override var progress: Progress {
|
||||
history.append(.progress)
|
||||
return super.progress
|
||||
}
|
||||
|
||||
@available(macOS 10.13, iOS 11.0, *)
|
||||
override var earliestBeginDate: Date? {
|
||||
get {
|
||||
history.append(.getEarliestBeginDate)
|
||||
#if canImport(Darwin)
|
||||
return super.earliestBeginDate
|
||||
#else
|
||||
return nil // Deprecated in swift-corerlibs-foundation
|
||||
#endif
|
||||
}
|
||||
set {
|
||||
history.append(.setEarliestBeginDate(newValue))
|
||||
#if canImport(Darwin)
|
||||
super.earliestBeginDate = newValue
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 10.13, iOS 11.0, *)
|
||||
override var countOfBytesClientExpectsToSend: Int64 {
|
||||
get {
|
||||
history.append(.getCountOfBytesClientExpectsToSend)
|
||||
return super.countOfBytesClientExpectsToSend
|
||||
}
|
||||
set {
|
||||
history.append(.setCountOfBytesClientExpectsToSend(newValue))
|
||||
super.countOfBytesClientExpectsToSend = newValue
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 10.13, iOS 11.0, *)
|
||||
override var countOfBytesClientExpectsToReceive: Int64 {
|
||||
get {
|
||||
history.append(.getCountOfBytesClientExpectsToReceive)
|
||||
return super.countOfBytesClientExpectsToReceive
|
||||
}
|
||||
set {
|
||||
history.append(.setCountOfBytesClientExpectsToReceive(newValue))
|
||||
super.countOfBytesClientExpectsToReceive = newValue
|
||||
}
|
||||
}
|
||||
|
||||
override var countOfBytesReceived: Int64 {
|
||||
history.append(.countOfBytesReceived)
|
||||
return super.countOfBytesReceived
|
||||
}
|
||||
|
||||
override var countOfBytesSent: Int64 {
|
||||
history.append(.countOfBytesSent)
|
||||
return super.countOfBytesSent
|
||||
}
|
||||
|
||||
override var countOfBytesExpectedToSend: Int64 {
|
||||
history.append(.countOfBytesExpectedToSend)
|
||||
return super.countOfBytesExpectedToSend
|
||||
}
|
||||
|
||||
override var countOfBytesExpectedToReceive: Int64 {
|
||||
history.append(.countOfBytesExpectedToReceive)
|
||||
return super.countOfBytesExpectedToReceive
|
||||
}
|
||||
|
||||
override var taskDescription: String? {
|
||||
get {
|
||||
history.append(.getTaskDescription)
|
||||
return super.taskDescription
|
||||
}
|
||||
set {
|
||||
history.append(.setTaskDescription(newValue))
|
||||
super.taskDescription = newValue
|
||||
}
|
||||
}
|
||||
|
||||
override func cancel() {
|
||||
history.append(.cancel)
|
||||
}
|
||||
|
||||
override var state: URLSessionTask.State {
|
||||
history.append(.state)
|
||||
return super.state
|
||||
}
|
||||
|
||||
override var error: Error? {
|
||||
history.append(.error)
|
||||
return super.error
|
||||
}
|
||||
|
||||
override func suspend() {
|
||||
history.append(.suspend)
|
||||
}
|
||||
|
||||
override func resume() {
|
||||
history.append(.resume)
|
||||
}
|
||||
|
||||
override var priority: Float {
|
||||
get {
|
||||
history.append(.getPriority)
|
||||
return super.priority
|
||||
}
|
||||
set {
|
||||
history.append(.setPriority(newValue))
|
||||
super.priority = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension URLError: EquatableError {}
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST || !canImport(Combine)
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private func makePublisher(
|
||||
_ session: URLSession,
|
||||
_ url: URL
|
||||
) -> URLSession.DataTaskPublisher {
|
||||
return session.dataTaskPublisher(for: url)
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private func makePublisher(
|
||||
_ session: URLSession,
|
||||
_ request: URLRequest
|
||||
) -> URLSession.DataTaskPublisher {
|
||||
return session.dataTaskPublisher(for: request)
|
||||
}
|
||||
#else
|
||||
private func makePublisher(
|
||||
_ session: URLSession,
|
||||
_ url: URL
|
||||
) -> URLSession.OCombine.DataTaskPublisher {
|
||||
return session.ocombine.dataTaskPublisher(for: url)
|
||||
}
|
||||
|
||||
private func makePublisher(
|
||||
_ session: URLSession,
|
||||
_ request: URLRequest
|
||||
) -> URLSession.OCombine.DataTaskPublisher {
|
||||
return session.ocombine.dataTaskPublisher(for: request)
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif // canImport(Darwin)
|
||||
@@ -164,13 +164,6 @@ extension XCTest {
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.cancelled, .cancelled])
|
||||
|
||||
let thirdSubscription = CustomSubscription()
|
||||
|
||||
try XCTUnwrap(helper.publisher.subscriber)
|
||||
.receive(subscription: thirdSubscription)
|
||||
|
||||
XCTAssertEqual(thirdSubscription.history, [.cancelled])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,13 +34,15 @@ import OpenCombine
|
||||
typealias CustomPublisher = CustomPublisherBase<Int, TestingError>
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
class CustomPublisherBase<Output, Failure: Error>: Publisher {
|
||||
class CustomPublisherBase<Output, Failure: Error>: Publisher, Cancellable {
|
||||
|
||||
private(set) var subscriber: AnySubscriber<Output, Failure>?
|
||||
private(set) var erasedSubscriber: Any?
|
||||
private let subscription: Subscription?
|
||||
|
||||
var onSubscribe: ((AnySubscriber<Output, Failure>) -> Void)?
|
||||
var willSubscribe: ((AnySubscriber<Output, Failure>) -> Void)?
|
||||
|
||||
var didSubscribe: ((AnySubscriber<Output, Failure>) -> Void)?
|
||||
|
||||
required init(subscription: Subscription?) {
|
||||
self.subscription = subscription
|
||||
@@ -51,9 +53,10 @@ class CustomPublisherBase<Output, Failure: Error>: Publisher {
|
||||
{
|
||||
let anySubscriber = AnySubscriber(subscriber)
|
||||
self.subscriber = anySubscriber
|
||||
onSubscribe?(anySubscriber)
|
||||
willSubscribe?(anySubscriber)
|
||||
erasedSubscriber = subscriber
|
||||
subscription.map(subscriber.receive(subscription:))
|
||||
didSubscribe?(anySubscriber)
|
||||
}
|
||||
|
||||
func send(subscription: CustomSubscription) {
|
||||
@@ -67,6 +70,11 @@ class CustomPublisherBase<Output, Failure: Error>: Publisher {
|
||||
func send(completion: Subscribers.Completion<Failure>) {
|
||||
subscriber?.receive(completion: completion)
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
subscriber = nil
|
||||
erasedSubscriber = nil
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
//
|
||||
// FairPriorityQueue.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 02.12.2019.
|
||||
//
|
||||
|
||||
/// A priproty queue based on binary min-heap.
|
||||
/// If two elements with the same priority are added, the element that was added
|
||||
/// earlier has will have "better" priority (i. e. it will be also extracted earlier).
|
||||
struct FairPriorityQueue<Priority: Comparable, Element> {
|
||||
|
||||
private var storage: [((Priority, UInt), Element)] = []
|
||||
private var next: UInt = 0
|
||||
|
||||
init() {}
|
||||
|
||||
mutating func insert(_ element: Element, priority: Priority) {
|
||||
storage.append(((priority, next), element))
|
||||
next += 1
|
||||
var newElementIndex = storage.endIndex - 1
|
||||
while let parent = self.parent(of: newElementIndex),
|
||||
storage[parent].0 > storage[newElementIndex].0 {
|
||||
storage.swapAt(newElementIndex, parent)
|
||||
newElementIndex = parent
|
||||
}
|
||||
}
|
||||
|
||||
func min() -> Element? {
|
||||
return storage.first?.1
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
mutating func extractMin() -> (Priority, Element)? {
|
||||
guard let max = storage.first else { return nil }
|
||||
storage[0] = storage[storage.endIndex - 1]
|
||||
storage.removeLast()
|
||||
minHeapify(0)
|
||||
return (max.0.0, max.1)
|
||||
}
|
||||
|
||||
var count: Int {
|
||||
return storage.count
|
||||
}
|
||||
|
||||
var isEmpty: Bool {
|
||||
return storage.isEmpty
|
||||
}
|
||||
|
||||
private func leftChild(of index: Int) -> Int? {
|
||||
assert(index >= 0)
|
||||
let childIndex = 2 * index + 1
|
||||
return childIndex < storage.endIndex ? childIndex : nil
|
||||
}
|
||||
|
||||
private func rightChild(of index: Int) -> Int? {
|
||||
assert(index >= 0)
|
||||
let childIndex = 2 * index + 2
|
||||
return childIndex < storage.endIndex ? childIndex : nil
|
||||
}
|
||||
|
||||
private func parent(of index: Int) -> Int? {
|
||||
assert(index >= 0)
|
||||
if index == 0 { return nil }
|
||||
return (index - 1) / 2
|
||||
}
|
||||
|
||||
private mutating func minHeapify(_ root: Int) {
|
||||
var root = root
|
||||
var largest = root
|
||||
while true {
|
||||
assert(largest == root)
|
||||
if let left = leftChild(of: root), storage[root].0 > storage[left].0 {
|
||||
largest = left
|
||||
}
|
||||
if let right = rightChild(of: root), storage[largest].0 > storage[right].0 {
|
||||
largest = right
|
||||
}
|
||||
if largest == root {
|
||||
break
|
||||
}
|
||||
storage.swapAt(root, largest)
|
||||
root = largest
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension FairPriorityQueue: Sequence {
|
||||
struct Iterator: IteratorProtocol {
|
||||
private var queue: FairPriorityQueue
|
||||
|
||||
fileprivate init(_ queue: FairPriorityQueue) {
|
||||
self.queue = queue
|
||||
}
|
||||
|
||||
mutating func next() -> (Priority, Element)? {
|
||||
return queue.extractMin()
|
||||
}
|
||||
}
|
||||
|
||||
func makeIterator() -> Iterator {
|
||||
return Iterator(self)
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ func testLifecycle<UpstreamOutput, Operator: Publisher>(
|
||||
line: UInt = #line,
|
||||
sendValue valueToBeSent: UpstreamOutput,
|
||||
cancellingSubscriptionReleasesSubscriber: Bool,
|
||||
finishingIsPassedThrough: Bool = true,
|
||||
_ makeOperator: (PassthroughSubject<UpstreamOutput, TestingError>) -> Operator
|
||||
) throws {
|
||||
var deinitCounter = 0
|
||||
@@ -101,7 +102,11 @@ func testLifecycle<UpstreamOutput, Operator: Publisher>(
|
||||
file: file,
|
||||
line: line)
|
||||
|
||||
try XCTUnwrap(subscription, file: file, line: line).cancel()
|
||||
try XCTUnwrap(subscription,
|
||||
"Lifecycle test #3: subscription should be saved",
|
||||
file: file,
|
||||
line: line)
|
||||
.cancel()
|
||||
|
||||
if cancellingSubscriptionReleasesSubscriber {
|
||||
XCTAssertEqual(deinitCounter,
|
||||
@@ -134,8 +139,15 @@ func testLifecycle<UpstreamOutput, Operator: Publisher>(
|
||||
passthrough.send(completion: .finished)
|
||||
}
|
||||
|
||||
XCTAssertTrue(subscriberDestroyed,
|
||||
"Lifecycle test #4: deinit should be called",
|
||||
file: file,
|
||||
line: line)
|
||||
if finishingIsPassedThrough {
|
||||
XCTAssertTrue(subscriberDestroyed,
|
||||
"Lifecycle test #4: deinit should be called",
|
||||
file: file,
|
||||
line: line)
|
||||
} else {
|
||||
XCTAssertFalse(subscriberDestroyed,
|
||||
"Lifecycle test #4: deinit should not be called",
|
||||
file: file,
|
||||
line: line)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,10 +15,10 @@ import OpenCombine
|
||||
|
||||
let childrenIsEmpty: (Mirror) -> Bool = { $0.children.isEmpty }
|
||||
|
||||
enum ExpectedMirrorChildValue: Equatable, ExpressibleByStringLiteral {
|
||||
enum ExpectedMirrorChildValue: ExpressibleByStringLiteral {
|
||||
case anything
|
||||
case matches(String)
|
||||
case contains(String)
|
||||
case matches(@autoclosure () -> String)
|
||||
case contains(@autoclosure () -> String)
|
||||
|
||||
typealias StringLiteralType = String
|
||||
|
||||
@@ -49,10 +49,11 @@ func expectedChildren(_ expectedChildren: (String?, ExpectedMirrorChildValue)...
|
||||
case (_, .anything):
|
||||
continue
|
||||
case let (lhs, .matches(rhs)):
|
||||
XCTAssertEqual(lhs, rhs, file: file, line: line)
|
||||
XCTAssertEqual(lhs, rhs(), file: file, line: line)
|
||||
case let (lhs, .contains(rhs)):
|
||||
XCTAssert(lhs.contains(rhs),
|
||||
"\"\(lhs)\" doesn't contain substring \"\(rhs)\"",
|
||||
let evaluatedRHS = rhs()
|
||||
XCTAssert(lhs.contains(evaluatedRHS),
|
||||
"\"\(lhs)\" doesn't contain substring \"\(evaluatedRHS)\"",
|
||||
file: file,
|
||||
line: line)
|
||||
}
|
||||
@@ -79,9 +80,9 @@ internal func testReflection<Output, Failure: Error, Operator: Publisher>(
|
||||
line: UInt = #line,
|
||||
parentInput: Output.Type,
|
||||
parentFailure: Failure.Type,
|
||||
description expectedDescription: String,
|
||||
description expectedDescription: String?,
|
||||
customMirror customMirrorPredicate: ((Mirror) -> Bool)?,
|
||||
playgroundDescription: String,
|
||||
playgroundDescription: String?,
|
||||
_ makeOperator: (CustomConnectablePublisherBase<Output, Failure>) -> Operator
|
||||
) throws {
|
||||
let publisher = CustomConnectablePublisherBase<Output, Failure>(subscription: nil)
|
||||
@@ -97,16 +98,18 @@ internal func testReflection<Output, Failure: Error, Operator: Publisher>(
|
||||
file: file,
|
||||
line: line)
|
||||
|
||||
let customMirror =
|
||||
try XCTUnwrap((erasedSubscriber as? CustomReflectable)?.customMirror,
|
||||
file: file,
|
||||
line: line)
|
||||
|
||||
if let customMirrorPredicate = customMirrorPredicate {
|
||||
let customMirror =
|
||||
try XCTUnwrap((erasedSubscriber as? CustomReflectable)?.customMirror,
|
||||
file: file,
|
||||
line: line)
|
||||
XCTAssert(customMirrorPredicate(customMirror),
|
||||
"customMirror doesn't satisfy the predicate",
|
||||
file: file,
|
||||
line: line)
|
||||
} else {
|
||||
XCTAssertFalse(erasedSubscriber is CustomReflectable,
|
||||
"subscriber shouldn't conform to CustomReflectable")
|
||||
}
|
||||
|
||||
XCTAssertFalse(erasedSubscriber is CustomDebugStringConvertible,
|
||||
@@ -131,7 +134,7 @@ internal func testSubscriptionReflection<Sut: Publisher>(
|
||||
customMirror customMirrorPredicate: ((Mirror) -> Bool)?,
|
||||
playgroundDescription: String,
|
||||
sut: Sut
|
||||
) throws where Sut.Output: Equatable {
|
||||
) throws {
|
||||
let tracking = TrackingSubscriberBase<Sut.Output, Sut.Failure>()
|
||||
sut.subscribe(tracking)
|
||||
|
||||
|
||||
@@ -40,6 +40,20 @@ extension TestingError: ExpressibleByStringLiteral {
|
||||
}
|
||||
}
|
||||
|
||||
protocol EquatableError: Error {
|
||||
func isEqual(_ other: EquatableError) -> Bool
|
||||
}
|
||||
|
||||
extension EquatableError where Self: Equatable {
|
||||
func isEqual(_ other: EquatableError) -> Bool {
|
||||
return self == (other as? Self)
|
||||
}
|
||||
}
|
||||
|
||||
extension TestingError: EquatableError {}
|
||||
|
||||
extension NSError: EquatableError {}
|
||||
|
||||
func assertThrowsError<Result>(_ expression: @autoclosure () throws -> Result,
|
||||
_ expected: TestingError,
|
||||
_ message: @autoclosure () -> String = "",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
// Created by Sergej Jaskiewicz on 11.06.2019.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
@@ -177,6 +178,10 @@ final class TrackingSubscriberBase<Value, Failure: Error>
|
||||
for subscription in subscriptions {
|
||||
subscription.cancel()
|
||||
}
|
||||
clearHistory()
|
||||
}
|
||||
|
||||
func clearHistory() {
|
||||
history = []
|
||||
}
|
||||
|
||||
@@ -220,8 +225,8 @@ extension TrackingSubscriberBase.Event {
|
||||
switch (lhs, rhs) {
|
||||
case (.finished, .finished):
|
||||
return true
|
||||
case let (.failure(lhs), .failure(rhs)):
|
||||
return (lhs as? TestingError) == (rhs as? TestingError)
|
||||
case let (.failure(lhs as EquatableError), .failure(rhs as EquatableError)):
|
||||
return lhs.isEqual(rhs)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
@@ -277,8 +282,9 @@ final class TrackingSubjectBase<Output: Equatable, Failure: Error>
|
||||
switch (lhs, rhs) {
|
||||
case (.finished, .finished):
|
||||
return true
|
||||
case let (.failure(lhs), .failure(rhs)):
|
||||
return (lhs as? TestingError) == (rhs as? TestingError)
|
||||
case let (.failure(lhs as EquatableError),
|
||||
.failure(rhs as EquatableError)):
|
||||
return lhs.isEqual(rhs)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -0,0 +1,316 @@
|
||||
//
|
||||
// VirtualTimeScheduler.swift
|
||||
// OpenCombineTests
|
||||
//
|
||||
// Created by Евгений Богомолов on 14/09/2019.
|
||||
//
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class VirtualTimeScheduler: Scheduler {
|
||||
|
||||
struct SchedulerTimeType: Strideable,
|
||||
Comparable,
|
||||
Hashable,
|
||||
SchedulerTimeIntervalConvertible
|
||||
{
|
||||
|
||||
struct Stride: ExpressibleByFloatLiteral,
|
||||
Comparable,
|
||||
SignedNumeric,
|
||||
SchedulerTimeIntervalConvertible
|
||||
{
|
||||
var magnitude: Int64
|
||||
|
||||
fileprivate init(magnitude: Int64) {
|
||||
self.magnitude = magnitude
|
||||
}
|
||||
|
||||
init(integerLiteral value: Int) {
|
||||
self = .seconds(value)
|
||||
}
|
||||
|
||||
init(floatLiteral value: Double) {
|
||||
self = .seconds(value)
|
||||
}
|
||||
|
||||
init?<Source: BinaryInteger>(exactly source: Source) {
|
||||
guard let magnitude = Int64(exactly: source) else {
|
||||
return nil
|
||||
}
|
||||
self.init(magnitude: magnitude)
|
||||
}
|
||||
|
||||
static func < (lhs: Stride, rhs: Stride) -> Bool {
|
||||
return lhs.magnitude < rhs.magnitude
|
||||
}
|
||||
|
||||
static func * (lhs: Stride, rhs: Stride) -> Stride {
|
||||
return Stride(magnitude: lhs.magnitude * rhs.magnitude)
|
||||
}
|
||||
|
||||
static func + (lhs: Stride, rhs: Stride) -> Stride {
|
||||
return Stride(magnitude: lhs.magnitude + rhs.magnitude)
|
||||
}
|
||||
|
||||
static func - (lhs: Stride, rhs: Stride) -> Stride {
|
||||
return Stride(magnitude: lhs.magnitude - rhs.magnitude)
|
||||
}
|
||||
|
||||
static func -= (lhs: inout Stride, rhs: Stride) {
|
||||
lhs.magnitude -= rhs.magnitude
|
||||
}
|
||||
|
||||
static func *= (lhs: inout Stride, rhs: Stride) {
|
||||
lhs.magnitude *= rhs.magnitude
|
||||
}
|
||||
|
||||
static func += (lhs: inout Stride, rhs: Stride) {
|
||||
lhs.magnitude += rhs.magnitude
|
||||
}
|
||||
|
||||
static func seconds(_ value: Int) -> Stride {
|
||||
return Stride(magnitude: Int64(value) * 1_000_000_000)
|
||||
}
|
||||
|
||||
static func seconds(_ value: Double) -> Stride {
|
||||
return Stride(magnitude: Int64(value * 1_000_000_000))
|
||||
}
|
||||
|
||||
static func milliseconds(_ value: Int) -> Stride {
|
||||
return Stride(magnitude: Int64(value) * 1_000_000)
|
||||
}
|
||||
|
||||
static func microseconds(_ value: Int) -> Stride {
|
||||
return Stride(magnitude: Int64(value) * 1_000)
|
||||
}
|
||||
|
||||
static func nanoseconds(_ value: Int) -> Stride {
|
||||
return Stride(magnitude: Int64(value))
|
||||
}
|
||||
}
|
||||
|
||||
/// Time in virtual nanoseconds
|
||||
let time: UInt64
|
||||
|
||||
private init(nanoseconds time: UInt64) {
|
||||
self.time = time
|
||||
}
|
||||
|
||||
static func == (lhs: SchedulerTimeType, rhs: SchedulerTimeType) -> Bool {
|
||||
return lhs.time == rhs.time
|
||||
}
|
||||
|
||||
static func < (lhs: SchedulerTimeType, rhs: SchedulerTimeType) -> Bool {
|
||||
return lhs.time < rhs.time
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(time)
|
||||
}
|
||||
|
||||
func distance(to other: SchedulerTimeType) -> Stride {
|
||||
if self > other {
|
||||
return Stride(magnitude: -Int64(time - other.time))
|
||||
} else {
|
||||
return Stride(magnitude: Int64(other.time - time))
|
||||
}
|
||||
}
|
||||
|
||||
func advanced(by stride: Stride) -> SchedulerTimeType {
|
||||
return stride.magnitude < 0
|
||||
? SchedulerTimeType(nanoseconds: time - UInt64(-stride.magnitude))
|
||||
: SchedulerTimeType(nanoseconds: time + UInt64(stride.magnitude))
|
||||
}
|
||||
|
||||
static func + (lhs: SchedulerTimeType, rhs: Stride) -> SchedulerTimeType {
|
||||
return lhs.advanced(by: rhs)
|
||||
}
|
||||
|
||||
static let beginningOfTime = SchedulerTimeType(nanoseconds: 0)
|
||||
|
||||
static func seconds(_ value: Int) -> SchedulerTimeType {
|
||||
precondition(value >= 0, "value must not be negative")
|
||||
return .init(nanoseconds: UInt64(value) * 1_000_000_000)
|
||||
}
|
||||
|
||||
static func seconds(_ value: Double) -> SchedulerTimeType {
|
||||
precondition(value >= 0, "value must not be negative")
|
||||
return .init(nanoseconds: UInt64(value * 1_000_000_000))
|
||||
}
|
||||
|
||||
static func milliseconds(_ value: Int) -> SchedulerTimeType {
|
||||
precondition(value >= 0, "value must not be negative")
|
||||
return .init(nanoseconds: UInt64(value) * 1_000_000)
|
||||
}
|
||||
|
||||
static func microseconds(_ value: Int) -> SchedulerTimeType {
|
||||
precondition(value >= 0, "value must not be negative")
|
||||
return .init(nanoseconds: UInt64(value) * 1_000)
|
||||
}
|
||||
|
||||
static func nanoseconds(_ value: Int) -> SchedulerTimeType {
|
||||
precondition(value >= 0, "value must not be negative")
|
||||
return .init(nanoseconds: UInt64(value))
|
||||
}
|
||||
}
|
||||
|
||||
enum SchedulerOptions: Equatable, CustomStringConvertible {
|
||||
case nontrivialOptions
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .nontrivialOptions:
|
||||
return ".nontrivialOptions"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class CancellableToken: Cancellable {
|
||||
|
||||
private(set) var isCancelled = false
|
||||
|
||||
func cancel() {
|
||||
isCancelled = true
|
||||
}
|
||||
}
|
||||
|
||||
enum Event: Equatable, CustomStringConvertible {
|
||||
case now
|
||||
case minimumTolerance
|
||||
case schedule(options: SchedulerOptions?)
|
||||
case scheduleAfterDate(SchedulerTimeType,
|
||||
tolerance: SchedulerTimeType.Stride,
|
||||
options: SchedulerOptions?)
|
||||
case scheduleAfterDateWithInterval(SchedulerTimeType,
|
||||
interval: SchedulerTimeType.Stride,
|
||||
tolerance: SchedulerTimeType.Stride,
|
||||
options: SchedulerOptions?)
|
||||
|
||||
var description: String {
|
||||
|
||||
func describeOptions(_ options: SchedulerOptions?) -> String {
|
||||
return options.map(String.init(describing:)) ?? "nil"
|
||||
}
|
||||
|
||||
func describeDate(_ date: SchedulerTimeType) -> String {
|
||||
return ".nanoseconds(\(date.time)"
|
||||
}
|
||||
|
||||
func describeStride(_ stride: SchedulerTimeType.Stride) -> String {
|
||||
return ".nanoseconds(\(stride.magnitude))"
|
||||
}
|
||||
|
||||
switch self {
|
||||
case .now:
|
||||
return ".now"
|
||||
case .minimumTolerance:
|
||||
return ".minimumTolerance"
|
||||
case let .schedule(options):
|
||||
return ".schedule(options: \(describeOptions(options)))"
|
||||
case let .scheduleAfterDate(date, tolerance, options):
|
||||
return """
|
||||
.scheduleAfterDate(\(describeDate(date)), \
|
||||
tolerance: \(describeStride(tolerance)), \
|
||||
options: \(describeOptions(options)))
|
||||
"""
|
||||
case let .scheduleAfterDateWithInterval(date, interval, tolerance, options):
|
||||
return """
|
||||
.scheduleAfterDateWithInterval(\(describeDate(date)), \
|
||||
interval: \(describeStride(interval)), \
|
||||
tolerance: \(describeStride(tolerance)), \
|
||||
options: \(describeOptions(options)))
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private(set) var history = [Event]()
|
||||
|
||||
/// All private methods should reference this property instead of `now`
|
||||
/// to prevent polluting the scheduler history. Accessing `now` creates an entry
|
||||
/// in the `history` array.
|
||||
private var _now = SchedulerTimeType.beginningOfTime
|
||||
|
||||
private var workQueue = FairPriorityQueue<SchedulerTimeType, () -> Void>()
|
||||
|
||||
var scheduledDates: [SchedulerTimeType] {
|
||||
return workQueue.map { $0.0 }
|
||||
}
|
||||
|
||||
var now: SchedulerTimeType {
|
||||
history.append(.now)
|
||||
return _now
|
||||
}
|
||||
|
||||
var minimumTolerance: SchedulerTimeType.Stride {
|
||||
history.append(.minimumTolerance)
|
||||
return 0
|
||||
}
|
||||
|
||||
func schedule(options: SchedulerOptions?, _ action: @escaping () -> Void) {
|
||||
history.append(.schedule(options: options))
|
||||
workQueue.insert(action, priority: _now)
|
||||
}
|
||||
|
||||
func schedule(after date: SchedulerTimeType,
|
||||
tolerance: SchedulerTimeType.Stride,
|
||||
options: SchedulerOptions?,
|
||||
_ action: @escaping () -> Void) {
|
||||
history.append(.scheduleAfterDate(date, tolerance: tolerance, options: options))
|
||||
workQueue.insert(action, priority: date)
|
||||
}
|
||||
|
||||
func schedule(after date: SchedulerTimeType,
|
||||
interval: SchedulerTimeType.Stride,
|
||||
tolerance: SchedulerTimeType.Stride,
|
||||
options: SchedulerOptions?,
|
||||
_ action: @escaping () -> Void) -> Cancellable {
|
||||
history.append(.scheduleAfterDateWithInterval(date,
|
||||
interval: interval,
|
||||
tolerance: tolerance,
|
||||
options: options))
|
||||
let cancellableToken = CancellableToken()
|
||||
repeatedlyExecute(after: date,
|
||||
interval: interval,
|
||||
cancellableToken: cancellableToken,
|
||||
action: action)
|
||||
return cancellableToken
|
||||
}
|
||||
|
||||
private func repeatedlyExecute(after date: SchedulerTimeType,
|
||||
interval: SchedulerTimeType.Stride,
|
||||
cancellableToken: CancellableToken,
|
||||
action: @escaping () -> Void) {
|
||||
let enqueuedAction: () -> Void = { [unowned self] in
|
||||
if cancellableToken.isCancelled { return }
|
||||
action()
|
||||
self.repeatedlyExecute(after: date + interval,
|
||||
interval: interval,
|
||||
cancellableToken: cancellableToken,
|
||||
action: action)
|
||||
}
|
||||
workQueue.insert(enqueuedAction, priority: date)
|
||||
}
|
||||
|
||||
/// Sets `now` to the provided value. Useful for testing that an entity that
|
||||
/// uses the scheduler doesn't rely on clock monotonicity.
|
||||
///
|
||||
/// - Note: The actions that were already executed will not be executed again.
|
||||
/// This function does **not** provide time machine-like functionality.
|
||||
func rewind(to time: SchedulerTimeType) {
|
||||
_now = time
|
||||
}
|
||||
|
||||
func executeScheduledActions() {
|
||||
while let (time, action) = workQueue.extractMin() {
|
||||
_now = max(time, _now)
|
||||
action()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,19 @@ import XCTest
|
||||
|
||||
// FIXME: XCTUnwrap is unavailable in Swift Package Manager yet.
|
||||
|
||||
private struct UnwrappingFailure: Error {}
|
||||
private struct UnwrappingFailure: Error, LocalizedError {
|
||||
|
||||
let message: String
|
||||
|
||||
var errorDescription: String? {
|
||||
var failureDescription = "XCTUnwrap failed"
|
||||
if !message.isEmpty {
|
||||
failureDescription += ": "
|
||||
failureDescription += message
|
||||
}
|
||||
return failureDescription
|
||||
}
|
||||
}
|
||||
|
||||
/// Asserts that an expression is not `nil`, and returns its unwrapped value.
|
||||
///
|
||||
@@ -32,10 +44,11 @@ public func XCTUnwrap<Result>(_ expression: @autoclosure () throws -> Result?,
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line) throws -> Result {
|
||||
let result = try expression()
|
||||
XCTAssertNotNil(result, message(), file: file, line: line)
|
||||
if let result = result {
|
||||
return result
|
||||
} else {
|
||||
throw UnwrappingFailure()
|
||||
let error = UnwrappingFailure(message: message())
|
||||
XCTFail(error.errorDescription ?? "", file: file, line: line)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,22 +23,27 @@ final class ObservableObjectPublisherTests: XCTestCase {
|
||||
receiveSubscription: { downstreamSubscription1 = $0 }
|
||||
)
|
||||
publisher.subscribe(tracking1)
|
||||
tracking1.assertHistoryEqual([.subscription("PassthroughSubject")])
|
||||
tracking1.assertHistoryEqual([.subscription("ObservableObjectPublisher")])
|
||||
downstreamSubscription1?.request(.max(1))
|
||||
tracking1.assertHistoryEqual([.subscription("PassthroughSubject")])
|
||||
tracking1.assertHistoryEqual([.subscription("ObservableObjectPublisher")])
|
||||
publisher.send()
|
||||
tracking1.assertHistoryEqual([.subscription("PassthroughSubject"),
|
||||
tracking1.assertHistoryEqual([.subscription("ObservableObjectPublisher"),
|
||||
.signal])
|
||||
publisher.send()
|
||||
publisher.send()
|
||||
downstreamSubscription1?.request(.max(3))
|
||||
tracking1.assertHistoryEqual([.subscription("PassthroughSubject"),
|
||||
tracking1.assertHistoryEqual([.subscription("ObservableObjectPublisher"),
|
||||
.signal,
|
||||
.signal,
|
||||
.signal])
|
||||
publisher.send()
|
||||
publisher.send()
|
||||
publisher.send()
|
||||
publisher.send()
|
||||
tracking1.assertHistoryEqual([.subscription("PassthroughSubject"),
|
||||
tracking1.assertHistoryEqual([.subscription("ObservableObjectPublisher"),
|
||||
.signal,
|
||||
.signal,
|
||||
.signal,
|
||||
.signal,
|
||||
.signal,
|
||||
.signal,
|
||||
@@ -49,28 +54,48 @@ final class ObservableObjectPublisherTests: XCTestCase {
|
||||
receiveSubscription: { $0.request(.unlimited) }
|
||||
)
|
||||
publisher.subscribe(tracking2)
|
||||
tracking2.assertHistoryEqual([.subscription("PassthroughSubject")])
|
||||
tracking2.assertHistoryEqual([.subscription("ObservableObjectPublisher")])
|
||||
|
||||
publisher.send()
|
||||
tracking1.assertHistoryEqual([.subscription("PassthroughSubject"),
|
||||
tracking1.assertHistoryEqual([.subscription("ObservableObjectPublisher"),
|
||||
.signal,
|
||||
.signal,
|
||||
.signal,
|
||||
.signal,
|
||||
.signal,
|
||||
.signal,
|
||||
.signal,
|
||||
.signal])
|
||||
tracking2.assertHistoryEqual([.subscription("PassthroughSubject"),
|
||||
tracking2.assertHistoryEqual([.subscription("ObservableObjectPublisher"),
|
||||
.signal])
|
||||
|
||||
downstreamSubscription1?.cancel()
|
||||
publisher.send()
|
||||
tracking1.assertHistoryEqual([.subscription("PassthroughSubject"),
|
||||
tracking1.assertHistoryEqual([.subscription("ObservableObjectPublisher"),
|
||||
.signal,
|
||||
.signal,
|
||||
.signal,
|
||||
.signal,
|
||||
.signal,
|
||||
.signal,
|
||||
.signal,
|
||||
.signal])
|
||||
tracking2.assertHistoryEqual([.subscription("PassthroughSubject"),
|
||||
tracking2.assertHistoryEqual([.subscription("ObservableObjectPublisher"),
|
||||
.signal,
|
||||
.signal])
|
||||
|
||||
tracking1.cancel()
|
||||
tracking2.cancel()
|
||||
}
|
||||
|
||||
func testObservableObjectPublisherReflection() throws {
|
||||
try testSubscriptionReflection(
|
||||
description: "ObservableObjectPublisher",
|
||||
customMirror: expectedChildren(
|
||||
("downstream", .contains("TrackingSubscriberBase"))
|
||||
),
|
||||
playgroundDescription: "ObservableObjectPublisher",
|
||||
sut: ObservableObjectPublisher()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,11 +88,11 @@ final class PublishedTests: XCTestCase {
|
||||
receiveSubscription: { downstreamSubscription = $0 }
|
||||
)
|
||||
testObject.objectWillChange.subscribe(tracking1)
|
||||
tracking1.assertHistoryEqual([.subscription("PassthroughSubject")])
|
||||
tracking1.assertHistoryEqual([.subscription("ObservableObjectPublisher")])
|
||||
downstreamSubscription?.request(.max(2))
|
||||
tracking1.assertHistoryEqual([.subscription("PassthroughSubject")])
|
||||
tracking1.assertHistoryEqual([.subscription("ObservableObjectPublisher")])
|
||||
testObject.state = 100
|
||||
tracking1.assertHistoryEqual([.subscription("PassthroughSubject")])
|
||||
tracking1.assertHistoryEqual([.subscription("ObservableObjectPublisher")])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
//
|
||||
// AssertNoFailureTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 25.12.2019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class AssertNoFailureTests: XCTestCase {
|
||||
|
||||
func testPassThroughInput() throws {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .max(2),
|
||||
createSut: { $0.assertNoFailure() })
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(1), .max(2))
|
||||
XCTAssertEqual(helper.publisher.send(2), .max(2))
|
||||
XCTAssertEqual(helper.publisher.send(3), .max(2))
|
||||
helper.publisher.send(completion: .finished)
|
||||
helper.publisher.send(completion: .finished)
|
||||
|
||||
let subscription2 = CustomSubscription()
|
||||
helper.publisher.send(subscription: subscription2)
|
||||
helper.publisher.send(subscription: subscription2)
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(42))
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("CustomSubscription"),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3),
|
||||
.completion(.finished),
|
||||
.completion(.finished),
|
||||
.subscription("CustomSubscription"),
|
||||
.subscription("CustomSubscription")])
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [])
|
||||
|
||||
XCTAssertEqual(subscription2.history, [.requested(.max(42)),
|
||||
.cancelled,
|
||||
.cancelled])
|
||||
}
|
||||
|
||||
func testCrashesOnFailure() {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .max(2),
|
||||
createSut: { $0.assertNoFailure() })
|
||||
helper.publisher.send(completion: .finished)
|
||||
assertCrashes {
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
}
|
||||
}
|
||||
|
||||
func testAssertNoFailureReflection() throws {
|
||||
try testReflection(
|
||||
parentInput: Int.self,
|
||||
parentFailure: TestingError.self,
|
||||
description: "AssertNoFailure",
|
||||
customMirror: expectedChildren(
|
||||
("file", "SomeFile.swift"),
|
||||
("line", "1987"),
|
||||
("prefix", "PREFIX")
|
||||
),
|
||||
playgroundDescription: "AssertNoFailure",
|
||||
{ $0.assertNoFailure("PREFIX", file: "SomeFile.swift", line: 1987) }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
//
|
||||
// BreakpointTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 03.12.2019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class BreakpointTests: XCTestCase {
|
||||
|
||||
func testReceiveSubscription() {
|
||||
var shouldStop = false
|
||||
var counter = 0
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .max(1)) {
|
||||
$0.breakpoint(receiveSubscription: { _ in counter += 1; return shouldStop })
|
||||
}
|
||||
|
||||
XCTAssertNotNil(helper.sut.receiveSubscription)
|
||||
XCTAssertNil(helper.sut.receiveOutput)
|
||||
XCTAssertNil(helper.sut.receiveCompletion)
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(12), .max(1))
|
||||
helper.publisher.send(completion: .finished)
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
XCTAssertEqual(helper.publisher.send(21), .max(1))
|
||||
helper.publisher.send(subscription: CustomSubscription())
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("CustomSubscription"),
|
||||
.value(12),
|
||||
.completion(.finished),
|
||||
.completion(.failure(.oops)),
|
||||
.value(21),
|
||||
.subscription("CustomSubscription")])
|
||||
XCTAssertEqual(helper.subscription.history, [])
|
||||
shouldStop = true
|
||||
XCTAssertEqual(counter, 2)
|
||||
assertCrashes {
|
||||
helper.publisher.send(subscription: CustomSubscription())
|
||||
}
|
||||
}
|
||||
|
||||
func testReceiveValue() {
|
||||
var counter = 0
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .max(1)) {
|
||||
$0.breakpoint(receiveOutput: { counter += 1; return $0 < 0 })
|
||||
}
|
||||
|
||||
XCTAssertNil(helper.sut.receiveSubscription)
|
||||
XCTAssertNotNil(helper.sut.receiveOutput)
|
||||
XCTAssertNil(helper.sut.receiveCompletion)
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(12), .max(1))
|
||||
helper.publisher.send(completion: .finished)
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
XCTAssertEqual(helper.publisher.send(21), .max(1))
|
||||
helper.publisher.send(subscription: CustomSubscription())
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("CustomSubscription"),
|
||||
.value(12),
|
||||
.completion(.finished),
|
||||
.completion(.failure(.oops)),
|
||||
.value(21),
|
||||
.subscription("CustomSubscription")])
|
||||
XCTAssertEqual(helper.subscription.history, [])
|
||||
XCTAssertEqual(counter, 2)
|
||||
assertCrashes {
|
||||
_ = helper.publisher.send(-1)
|
||||
}
|
||||
}
|
||||
|
||||
func testReceiveCompletion() {
|
||||
var counter = 0
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .max(1)) {
|
||||
$0.breakpoint(receiveCompletion: { counter += 1; return $0 == .finished })
|
||||
}
|
||||
|
||||
XCTAssertNil(helper.sut.receiveSubscription)
|
||||
XCTAssertNil(helper.sut.receiveOutput)
|
||||
XCTAssertNotNil(helper.sut.receiveCompletion)
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(12), .max(1))
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
XCTAssertEqual(helper.publisher.send(21), .max(1))
|
||||
helper.publisher.send(subscription: CustomSubscription())
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("CustomSubscription"),
|
||||
.value(12),
|
||||
.completion(.failure(.oops)),
|
||||
.completion(.failure(.oops)),
|
||||
.value(21),
|
||||
.subscription("CustomSubscription")])
|
||||
XCTAssertEqual(counter, 2)
|
||||
assertCrashes {
|
||||
helper.publisher.send(completion: .finished)
|
||||
}
|
||||
}
|
||||
|
||||
func testBreakpointOnError() throws {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .max(1)) {
|
||||
$0.breakpointOnError()
|
||||
}
|
||||
|
||||
XCTAssertNil(helper.sut.receiveSubscription)
|
||||
XCTAssertNil(helper.sut.receiveOutput)
|
||||
XCTAssertNotNil(helper.sut.receiveCompletion)
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(12), .max(1))
|
||||
helper.publisher.send(completion: .finished)
|
||||
helper.publisher.send(completion: .finished)
|
||||
XCTAssertEqual(helper.publisher.send(21), .max(1))
|
||||
helper.publisher.send(subscription: CustomSubscription())
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("CustomSubscription"),
|
||||
.value(12),
|
||||
.completion(.finished),
|
||||
.completion(.finished),
|
||||
.value(21),
|
||||
.subscription("CustomSubscription")])
|
||||
|
||||
XCTAssertEqual(helper.sut.receiveCompletion?(.finished), false)
|
||||
XCTAssertEqual(helper.sut.receiveCompletion?(.failure(.oops)), true)
|
||||
|
||||
assertCrashes {
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
}
|
||||
}
|
||||
|
||||
func testCancelAlreadyCancelled() throws {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .none) {
|
||||
$0.breakpointOnError()
|
||||
}
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(14))
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(100))
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
|
||||
.requested(.max(14)),
|
||||
.cancelled,
|
||||
.requested(.max(100)),
|
||||
.cancelled])
|
||||
}
|
||||
|
||||
func testBreakpointReflection() throws {
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: Error.self,
|
||||
description: "Breakpoint",
|
||||
customMirror: expectedChildren(
|
||||
("upstream", .contains("CustomConnectablePublisherBase"))
|
||||
),
|
||||
playgroundDescription: "Breakpoint",
|
||||
{ $0.breakpointOnError() })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
//
|
||||
// CollectByCountTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 25.12.2019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class CollectByCountTests: XCTestCase {
|
||||
|
||||
func testBasicBehavior() {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(2),
|
||||
receiveValueDemand: .max(3),
|
||||
createSut: { $0.collect(4) })
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(8))])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("CollectByCount")])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(1), .none)
|
||||
XCTAssertEqual(helper.publisher.send(2), .none)
|
||||
XCTAssertEqual(helper.publisher.send(3), .none)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("CollectByCount")])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(4), .max(12))
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("CollectByCount"),
|
||||
.value([1, 2, 3, 4])])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(5), .none)
|
||||
XCTAssertEqual(helper.publisher.send(6), .none)
|
||||
XCTAssertEqual(helper.publisher.send(7), .none)
|
||||
XCTAssertEqual(helper.publisher.send(8), .max(12))
|
||||
XCTAssertEqual(helper.publisher.send(9), .none)
|
||||
XCTAssertEqual(helper.publisher.send(10), .none)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("CollectByCount"),
|
||||
.value([1, 2, 3, 4]),
|
||||
.value([5, 6, 7, 8])])
|
||||
|
||||
helper.publisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("CollectByCount"),
|
||||
.value([1, 2, 3, 4]),
|
||||
.value([5, 6, 7, 8]),
|
||||
.value([9, 10]),
|
||||
.completion(.finished)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(8))])
|
||||
}
|
||||
|
||||
func testFinishWithEmptyBuffer() throws {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .max(3),
|
||||
createSut: { $0.collect(4) })
|
||||
XCTAssertEqual(helper.publisher.send(1), .none)
|
||||
XCTAssertEqual(helper.publisher.send(2), .none)
|
||||
XCTAssertEqual(helper.publisher.send(3), .none)
|
||||
XCTAssertEqual(helper.publisher.send(4), .max(12))
|
||||
helper.publisher.send(completion: .finished)
|
||||
helper.publisher.send(completion: .finished)
|
||||
XCTAssertEqual(helper.publisher.send(5), .none)
|
||||
XCTAssertEqual(helper.publisher.send(6), .none)
|
||||
XCTAssertEqual(helper.publisher.send(7), .none)
|
||||
XCTAssertEqual(helper.publisher.send(8), .none)
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(2))
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("CollectByCount"),
|
||||
.value([1, 2, 3, 4]),
|
||||
.completion(.finished),
|
||||
.completion(.finished)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
}
|
||||
|
||||
func testFailureWithEmptyBuffer() throws {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .max(3),
|
||||
createSut: { $0.collect(4) })
|
||||
XCTAssertEqual(helper.publisher.send(1), .none)
|
||||
XCTAssertEqual(helper.publisher.send(2), .none)
|
||||
XCTAssertEqual(helper.publisher.send(3), .none)
|
||||
XCTAssertEqual(helper.publisher.send(4), .max(12))
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
XCTAssertEqual(helper.publisher.send(5), .none)
|
||||
XCTAssertEqual(helper.publisher.send(6), .none)
|
||||
XCTAssertEqual(helper.publisher.send(7), .none)
|
||||
XCTAssertEqual(helper.publisher.send(8), .none)
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(2))
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("CollectByCount"),
|
||||
.value([1, 2, 3, 4]),
|
||||
.completion(.failure(.oops)),
|
||||
.completion(.failure(.oops))])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
}
|
||||
|
||||
func testFailureWithNonEmptyBuffer() throws {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .max(3),
|
||||
createSut: { $0.collect(4) })
|
||||
XCTAssertEqual(helper.publisher.send(1), .none)
|
||||
XCTAssertEqual(helper.publisher.send(2), .none)
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
XCTAssertEqual(helper.publisher.send(5), .none)
|
||||
XCTAssertEqual(helper.publisher.send(6), .none)
|
||||
XCTAssertEqual(helper.publisher.send(7), .none)
|
||||
XCTAssertEqual(helper.publisher.send(8), .none)
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(2))
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("CollectByCount"),
|
||||
.completion(.failure(.oops)),
|
||||
.completion(.failure(.oops))])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
}
|
||||
|
||||
func testCrashesOnZeroDemand() throws {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .max(3),
|
||||
createSut: { $0.collect(4) })
|
||||
|
||||
try assertCrashes {
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.none)
|
||||
}
|
||||
}
|
||||
|
||||
func testCancelThenFinish() throws {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .max(3),
|
||||
createSut: { $0.collect(4) })
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(1), .none)
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
helper.publisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("CollectByCount"),
|
||||
.completion(.finished)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
|
||||
.cancelled])
|
||||
}
|
||||
|
||||
func testCancelAlreadyCancelled() throws {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .max(3),
|
||||
createSut: { $0.collect(4) })
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
|
||||
let subscription2 = CustomSubscription()
|
||||
helper.publisher.send(subscription: subscription2)
|
||||
XCTAssertEqual(subscription2.history, [.cancelled])
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(2))
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
helper.publisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("CollectByCount"),
|
||||
.completion(.finished)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
|
||||
.cancelled])
|
||||
}
|
||||
|
||||
func testCollectByCountReceiveValueBeforeSubscription() {
|
||||
testReceiveValueBeforeSubscription(value: 42,
|
||||
expected: .history([], demand: .none),
|
||||
{ $0.collect(19) })
|
||||
}
|
||||
|
||||
func testCollectByCountReceiveCompletionBeforeSubscription() {
|
||||
testReceiveCompletionBeforeSubscription(
|
||||
inputType: Int.self,
|
||||
expected: .history([.completion(.finished)]),
|
||||
{ $0.collect(19) }
|
||||
)
|
||||
}
|
||||
|
||||
func testCollectByCountRequestBeforeSubscription() {
|
||||
testRequestBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
{ $0.collect(19) })
|
||||
}
|
||||
|
||||
func testCollectByCountCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
{ $0.collect(19) })
|
||||
}
|
||||
|
||||
func testCollectByCountReceiveSubscriptionTwice() throws {
|
||||
try testReceiveSubscriptionTwice { $0.collect(19) }
|
||||
}
|
||||
|
||||
func testCollectByCountLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
{ $0.collect(42) })
|
||||
}
|
||||
|
||||
func testCollectByCountReflection() throws {
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: Error.self,
|
||||
description: "CollectByCount",
|
||||
customMirror: expectedChildren(
|
||||
("downstream", .contains("TrackingSubscriberBase")),
|
||||
("upstreamSubscription", "nil"),
|
||||
("buffer", "[]"),
|
||||
("count", "53")
|
||||
),
|
||||
playgroundDescription: "CollectByCount",
|
||||
{ $0.collect(53) })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,452 @@
|
||||
//
|
||||
// ConcatenateTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 09.12.2019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class ConcatenateTests: XCTestCase {
|
||||
|
||||
func testAppendBasicBehavior() {
|
||||
let subscription1 = CustomSubscription()
|
||||
let subscription2 = CustomSubscription()
|
||||
let publisher1 = CustomPublisher(subscription: subscription1)
|
||||
let publisher2 = CustomPublisher(subscription: subscription2)
|
||||
|
||||
let append = publisher1.append(publisher2)
|
||||
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: { $0.request(.max(10)) },
|
||||
receiveValue: { .max($0) },
|
||||
receiveCompletion: { _ in }
|
||||
)
|
||||
|
||||
append.subscribe(tracking)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Concatenate")])
|
||||
XCTAssertEqual(subscription1.history, [.requested(.max(10))])
|
||||
XCTAssertEqual(subscription2.history, [])
|
||||
|
||||
XCTAssertEqual(publisher1.send(1), .max(1))
|
||||
XCTAssertEqual(publisher2.send(-1), .none)
|
||||
XCTAssertEqual(publisher1.send(2), .max(2))
|
||||
XCTAssertEqual(publisher2.send(-2), .none)
|
||||
XCTAssertEqual(publisher1.send(3), .max(3))
|
||||
XCTAssertEqual(publisher2.send(-3), .none)
|
||||
publisher2.send(completion: .failure(.oops))
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Concatenate"),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3)])
|
||||
XCTAssertEqual(subscription1.history, [.requested(.max(10))])
|
||||
XCTAssertEqual(subscription2.history, [])
|
||||
|
||||
publisher1.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Concatenate"),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3)])
|
||||
XCTAssertEqual(subscription1.history, [.requested(.max(10))])
|
||||
XCTAssertEqual(subscription2.history, [.requested(.max(13))])
|
||||
|
||||
XCTAssertEqual(publisher1.send(4000), .max(4000))
|
||||
XCTAssertEqual(publisher2.send(5), .max(5))
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Concatenate"),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3),
|
||||
.value(4000),
|
||||
.value(5)])
|
||||
XCTAssertEqual(subscription1.history, [.requested(.max(10))])
|
||||
XCTAssertEqual(subscription2.history, [.requested(.max(13))])
|
||||
|
||||
publisher2.send(completion: .finished)
|
||||
publisher2.send(completion: .finished)
|
||||
publisher1.send(completion: .finished)
|
||||
publisher1.send(completion: .failure(.oops))
|
||||
publisher2.send(completion: .failure(.oops))
|
||||
|
||||
XCTAssertEqual(publisher1.send(6000), .max(6000))
|
||||
XCTAssertEqual(publisher2.send(7), .max(7))
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Concatenate"),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3),
|
||||
.value(4000),
|
||||
.value(5),
|
||||
.completion(.finished),
|
||||
.completion(.finished),
|
||||
.completion(.finished),
|
||||
.completion(.failure(.oops)),
|
||||
.completion(.failure(.oops)),
|
||||
.value(6000),
|
||||
.value(7)])
|
||||
XCTAssertEqual(subscription1.history, [.requested(.max(10))])
|
||||
XCTAssertEqual(subscription2.history, [.requested(.max(13))])
|
||||
|
||||
let subscription3 = CustomSubscription()
|
||||
publisher2.send(subscription: subscription3)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Concatenate"),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3),
|
||||
.value(4000),
|
||||
.value(5),
|
||||
.completion(.finished),
|
||||
.completion(.finished),
|
||||
.completion(.finished),
|
||||
.completion(.failure(.oops)),
|
||||
.completion(.failure(.oops)),
|
||||
.value(6000),
|
||||
.value(7)])
|
||||
XCTAssertEqual(subscription3.history, [.cancelled])
|
||||
}
|
||||
|
||||
func testConcatenateTwoSequences() {
|
||||
let sequence1: Publishers.Sequence =
|
||||
sequence(first: 1, next: { $0 > 20 ? nil : $0 * 2 })
|
||||
.publisher
|
||||
|
||||
let sequence2: Publishers.Sequence = (33 ..< 40).publisher
|
||||
|
||||
let expected = [1, 2, 4, 8, 16, 32, 33, 34, 35, 36, 37, 38, 39]
|
||||
|
||||
var historyAppend = [Int]()
|
||||
var appendCompleted = false
|
||||
let append: Publishers.Concatenate = sequence1.append(sequence2)
|
||||
let cancellableAppend = append
|
||||
.sink(receiveCompletion: { _ in appendCompleted = true },
|
||||
receiveValue: { historyAppend.append($0) })
|
||||
XCTAssertEqual(historyAppend, expected)
|
||||
XCTAssertTrue(appendCompleted)
|
||||
cancellableAppend.cancel()
|
||||
|
||||
var historyPrepend = [Int]()
|
||||
var prependCompleted = false
|
||||
let prepend: Publishers.Concatenate = sequence2.prepend(sequence1)
|
||||
let cancellablePrepend = prepend
|
||||
.sink(receiveCompletion: { _ in prependCompleted = true },
|
||||
receiveValue: { historyPrepend.append($0) })
|
||||
XCTAssertEqual(historyPrepend, [1, 2, 4, 8, 16, 32, 33, 34, 35, 36, 37, 38, 39])
|
||||
XCTAssertTrue(prependCompleted)
|
||||
cancellablePrepend.cancel()
|
||||
}
|
||||
|
||||
func testPrefixFailureFailsDownstream() {
|
||||
let subscription1 = CustomSubscription()
|
||||
let subscription2 = CustomSubscription()
|
||||
let publisher1 = CustomPublisher(subscription: subscription1)
|
||||
let publisher2 = CustomPublisher(subscription: subscription2)
|
||||
|
||||
let append = publisher1.append(publisher2)
|
||||
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: { $0.request(.max(10)) },
|
||||
receiveValue: { .max($0) },
|
||||
receiveCompletion: { _ in }
|
||||
)
|
||||
|
||||
append.subscribe(tracking)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Concatenate")])
|
||||
XCTAssertEqual(subscription1.history, [.requested(.max(10))])
|
||||
XCTAssertEqual(subscription2.history, [])
|
||||
|
||||
publisher1.send(completion: .failure(.oops))
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Concatenate"),
|
||||
.completion(.failure(.oops))])
|
||||
XCTAssertEqual(subscription1.history, [.requested(.max(10))])
|
||||
XCTAssertEqual(subscription2.history, [])
|
||||
|
||||
XCTAssertEqual(publisher1.send(2), .max(2))
|
||||
publisher1.send(completion: .finished)
|
||||
XCTAssertEqual(publisher2.send(3), .max(3))
|
||||
publisher2.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Concatenate"),
|
||||
.completion(.failure(.oops)),
|
||||
.value(2),
|
||||
.value(3),
|
||||
.completion(.finished)])
|
||||
XCTAssertEqual(subscription1.history, [.requested(.max(10))])
|
||||
XCTAssertEqual(subscription2.history, [.requested(.max(11))])
|
||||
}
|
||||
|
||||
func testSubscribesToUpstreamThenSendsSubscriptionDownstream() {
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let append = publisher.append(1, 2, 3)
|
||||
let tracking = TrackingSubscriber()
|
||||
|
||||
var didSubscribe = false
|
||||
|
||||
publisher.didSubscribe = { _ in
|
||||
XCTAssertEqual(tracking.history, [])
|
||||
didSubscribe = true
|
||||
}
|
||||
|
||||
XCTAssertEqual(tracking.history, [])
|
||||
|
||||
append.subscribe(tracking)
|
||||
|
||||
XCTAssertTrue(didSubscribe)
|
||||
XCTAssertEqual(tracking.history, [.subscription("Concatenate")])
|
||||
}
|
||||
|
||||
func testBackpressure() throws {
|
||||
let subscription1 = CustomSubscription()
|
||||
let subscription2 = CustomSubscription()
|
||||
let publisher1 = CustomPublisher(subscription: subscription1)
|
||||
let publisher2 = CustomPublisher(subscription: subscription2)
|
||||
|
||||
let append = publisher1.append(publisher2)
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: { downstreamSubscription = $0; $0.request(.max(10)) },
|
||||
receiveValue: { .max($0) },
|
||||
receiveCompletion: { _ in }
|
||||
)
|
||||
|
||||
publisher2.willSubscribe = { _ in
|
||||
downstreamSubscription?.request(.max(7))
|
||||
XCTAssertEqual(subscription1.history, [.requested(.max(10)),
|
||||
.requested(.none)])
|
||||
XCTAssertEqual(subscription2.history, [])
|
||||
}
|
||||
|
||||
append.subscribe(tracking)
|
||||
try XCTUnwrap(downstreamSubscription).request(.none)
|
||||
XCTAssertEqual(publisher1.send(3), .max(3))
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Concatenate"),
|
||||
.value(3)])
|
||||
XCTAssertEqual(subscription1.history, [.requested(.max(10)),
|
||||
.requested(.none)])
|
||||
XCTAssertEqual(subscription2.history, [])
|
||||
|
||||
publisher1.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(subscription1.history, [.requested(.max(10)),
|
||||
.requested(.none)])
|
||||
XCTAssertEqual(subscription2.history, [.requested(.max(19))])
|
||||
|
||||
try XCTUnwrap(downstreamSubscription).request(.unlimited)
|
||||
|
||||
XCTAssertEqual(subscription1.history, [.requested(.max(10)),
|
||||
.requested(.none)])
|
||||
XCTAssertEqual(subscription2.history, [.requested(.max(19)),
|
||||
.requested(.unlimited)])
|
||||
|
||||
publisher2.send(completion: .finished)
|
||||
|
||||
try XCTUnwrap(downstreamSubscription).request(.max(42))
|
||||
|
||||
XCTAssertEqual(subscription1.history, [.requested(.max(10)),
|
||||
.requested(.none)])
|
||||
XCTAssertEqual(subscription2.history, [.requested(.max(19)),
|
||||
.requested(.unlimited),
|
||||
.requested(.max(42))])
|
||||
}
|
||||
|
||||
func testCancelAlreadyCancelled() throws {
|
||||
let subscription1 = CustomSubscription()
|
||||
let subscription2 = CustomSubscription()
|
||||
let publisher1 = CustomPublisher(subscription: subscription1)
|
||||
let publisher2 = CustomPublisher(subscription: subscription2)
|
||||
|
||||
let append = publisher1.append(publisher2)
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: { downstreamSubscription = $0 },
|
||||
receiveValue: { .max($0) },
|
||||
receiveCompletion: { _ in }
|
||||
)
|
||||
append.subscribe(tracking)
|
||||
try XCTUnwrap(downstreamSubscription).cancel()
|
||||
try XCTUnwrap(downstreamSubscription).cancel()
|
||||
try XCTUnwrap(downstreamSubscription).request(.max(2)) // total demand is 2
|
||||
try XCTUnwrap(downstreamSubscription).cancel()
|
||||
try XCTUnwrap(downstreamSubscription).request(.max(3)) // total demand is 5
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Concatenate")])
|
||||
XCTAssertEqual(subscription1.history, [.cancelled])
|
||||
XCTAssertEqual(subscription2.history, [])
|
||||
|
||||
XCTAssertEqual(publisher1.send(0), .none) // total demand is 4
|
||||
publisher1.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Concatenate"),
|
||||
.value(0)])
|
||||
XCTAssertEqual(subscription1.history, [.cancelled])
|
||||
XCTAssertEqual(subscription2.history, [.requested(.max(4))])
|
||||
|
||||
XCTAssertEqual(publisher1.send(0), .none) // total demand is 3
|
||||
publisher1.send(completion: .failure(.oops))
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Concatenate"),
|
||||
.value(0),
|
||||
.value(0),
|
||||
.completion(.failure(.oops))])
|
||||
XCTAssertEqual(subscription1.history, [.cancelled])
|
||||
XCTAssertEqual(subscription2.history, [.requested(.max(4))])
|
||||
|
||||
try XCTUnwrap(downstreamSubscription).cancel()
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Concatenate"),
|
||||
.value(0),
|
||||
.value(0),
|
||||
.completion(.failure(.oops))])
|
||||
XCTAssertEqual(subscription1.history, [.cancelled])
|
||||
XCTAssertEqual(subscription2.history, [.requested(.max(4)),
|
||||
.cancelled])
|
||||
|
||||
let subscription3 = CustomSubscription()
|
||||
publisher2.send(subscription: subscription3)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Concatenate"),
|
||||
.value(0),
|
||||
.value(0),
|
||||
.completion(.failure(.oops))])
|
||||
XCTAssertEqual(subscription1.history, [.cancelled])
|
||||
XCTAssertEqual(subscription2.history, [.requested(.max(4)),
|
||||
.cancelled])
|
||||
XCTAssertEqual(subscription3.history, [.cancelled])
|
||||
}
|
||||
|
||||
func testRecursivelyReceiveValue() {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .none,
|
||||
createSut: { $0.append() })
|
||||
|
||||
var recursion = 10
|
||||
helper.tracking.onValue = {
|
||||
if recursion == 0 { return }
|
||||
recursion -= 1
|
||||
XCTAssertEqual(helper.publisher.send($0 + 1), .none)
|
||||
}
|
||||
|
||||
assertCrashes {
|
||||
XCTAssertEqual(helper.publisher.send(1), .none)
|
||||
}
|
||||
}
|
||||
|
||||
func testRecursivelyReceiveFailure() {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .none,
|
||||
createSut: { $0.append() })
|
||||
|
||||
var recursion = 10
|
||||
helper.tracking.onFailure = { _ in
|
||||
if recursion == 0 { return }
|
||||
recursion -= 1
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
}
|
||||
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Concatenate"),
|
||||
.completion(.failure(.oops)),
|
||||
.completion(.failure(.oops)),
|
||||
.completion(.failure(.oops)),
|
||||
.completion(.failure(.oops)),
|
||||
.completion(.failure(.oops)),
|
||||
.completion(.failure(.oops)),
|
||||
.completion(.failure(.oops)),
|
||||
.completion(.failure(.oops)),
|
||||
.completion(.failure(.oops)),
|
||||
.completion(.failure(.oops)),
|
||||
.completion(.failure(.oops))])
|
||||
XCTAssertEqual(helper.subscription.history, [])
|
||||
}
|
||||
|
||||
func testHelperMethods() {
|
||||
let publisher = CustomPublisher(subscription: nil)
|
||||
XCTAssertEqual(publisher.append(2, 3, 5, 7).suffix.sequence, [2, 3, 5, 7])
|
||||
XCTAssertEqual(publisher.append(CollectionOfOne(42)).suffix.sequence.first, 42)
|
||||
XCTAssertEqual(publisher.prepend(7, 5, 3, 2).prefix.sequence, [7, 5, 3, 2])
|
||||
XCTAssertEqual(publisher.prepend(CollectionOfOne(42)).prefix.sequence.first, 42)
|
||||
}
|
||||
|
||||
func testConcatenateReceiveValueBeforeSubscription() {
|
||||
testReceiveValueBeforeSubscription(value: 12,
|
||||
expected: .crash,
|
||||
{ $0.append(1, 2, 3) })
|
||||
|
||||
testReceiveValueBeforeSubscription(value: 12,
|
||||
expected: .crash,
|
||||
{ $0.prepend(1, 2, 3) })
|
||||
}
|
||||
|
||||
func testConcatenateReceiveCompletionBeforeSubscription() {
|
||||
testReceiveCompletionBeforeSubscription(
|
||||
inputType: Int.self,
|
||||
expected: .history([.subscription("Concatenate")]),
|
||||
{ $0.append(1, 2, 3) }
|
||||
)
|
||||
|
||||
testReceiveCompletionBeforeSubscription(
|
||||
inputType: Int.self,
|
||||
expected: .history([.subscription("Concatenate")]),
|
||||
{ $0.prepend(1, 2, 3) }
|
||||
)
|
||||
}
|
||||
|
||||
func testConcatenateRequestBeforeSubscription() {
|
||||
testRequestBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
{ $0.append(1, 2, 3) })
|
||||
}
|
||||
|
||||
func testConcatenateCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
{ $0.append(1, 2, 3) })
|
||||
}
|
||||
|
||||
func testConcatenateReceiveSubscriptionTwice() throws {
|
||||
try testReceiveSubscriptionTwice { $0.append(1, 2, 3) }
|
||||
}
|
||||
|
||||
func testConcatenateReflection() throws {
|
||||
try testReflection(parentInput: Float.self,
|
||||
parentFailure: TestingError.self,
|
||||
description: "Concatenate",
|
||||
customMirror: expectedChildren(
|
||||
("downstream", .contains("TrackingSubscriberBase")),
|
||||
("upstreamSubscription", "nil"),
|
||||
("suffix", .contains("(sequence: [2.0, 3.0, 5.0, 7.0])")),
|
||||
("demand", "max(0)")
|
||||
),
|
||||
playgroundDescription: "Concatenate",
|
||||
{ $0.append(2, 3, 5, 7) })
|
||||
}
|
||||
|
||||
func testConcatenateLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
finishingIsPassedThrough: true,
|
||||
{ $0.append() })
|
||||
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
finishingIsPassedThrough: false,
|
||||
{ $0.prepend(1, 2, 3) })
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
import XCTest
|
||||
|
||||
//import Combine
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
|
||||
@@ -0,0 +1,464 @@
|
||||
//
|
||||
// DelayTests.swift
|
||||
// OpenCombineTests
|
||||
//
|
||||
// Created by Евгений Богомолов on 08/09/2019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class DelayTests: XCTestCase {
|
||||
|
||||
// Delay's Inner doesn't conform to CustomStringConvertible, so we can't compare
|
||||
// subscriptions using their descriptions
|
||||
private let delaySubscription: StringSubscription = {
|
||||
let tracking = TrackingSubscriber()
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
CustomPublisher(subscription: CustomSubscription())
|
||||
.delay(for: 0, scheduler: scheduler)
|
||||
.subscribe(tracking)
|
||||
scheduler.executeScheduledActions()
|
||||
return tracking.subscriptions.first.map(StringSubscription.subscription)
|
||||
?? "Delay"
|
||||
}()
|
||||
|
||||
private let delaySubscriptionImmediateScheduler: StringSubscription = {
|
||||
let tracking = TrackingSubscriber()
|
||||
CustomPublisher(subscription: CustomSubscription())
|
||||
.delay(for: 0, scheduler: ImmediateScheduler.shared)
|
||||
.subscribe(tracking)
|
||||
return tracking.subscriptions.first.map(StringSubscription.subscription)
|
||||
?? "Delay"
|
||||
}()
|
||||
|
||||
func testBasicBehavior() {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(100),
|
||||
receiveValueDemand: .max(12)) {
|
||||
$0.delay(for: .nanoseconds(200),
|
||||
tolerance: .nanoseconds(5),
|
||||
scheduler: scheduler,
|
||||
options: .nontrivialOptions)
|
||||
}
|
||||
XCTAssertNotNil(helper.publisher.subscriber)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(delaySubscription)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(100))])
|
||||
XCTAssertEqual(scheduler.history, [])
|
||||
|
||||
scheduler.executeScheduledActions()
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(delaySubscription)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(100))])
|
||||
XCTAssertEqual(scheduler.history, [])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(1), .none)
|
||||
XCTAssertEqual(helper.publisher.send(2), .none)
|
||||
XCTAssertEqual(helper.publisher.send(3), .none)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(delaySubscription)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(100))])
|
||||
XCTAssertEqual(scheduler.scheduledDates, [.nanoseconds(200),
|
||||
.nanoseconds(200),
|
||||
.nanoseconds(200)])
|
||||
|
||||
XCTAssertEqual(scheduler.history,
|
||||
[.now,
|
||||
.scheduleAfterDate(.nanoseconds(200),
|
||||
tolerance: .nanoseconds(5),
|
||||
options: .nontrivialOptions),
|
||||
.now,
|
||||
.scheduleAfterDate(.nanoseconds(200),
|
||||
tolerance: .nanoseconds(5),
|
||||
options: .nontrivialOptions),
|
||||
.now,
|
||||
.scheduleAfterDate(.nanoseconds(200),
|
||||
tolerance: .nanoseconds(5),
|
||||
options: .nontrivialOptions)])
|
||||
|
||||
scheduler.executeScheduledActions()
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(delaySubscription),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3)])
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(100)),
|
||||
.requested(.max(12)),
|
||||
.requested(.max(12)),
|
||||
.requested(.max(12))])
|
||||
|
||||
XCTAssertEqual(scheduler.history,
|
||||
[.now,
|
||||
.scheduleAfterDate(.nanoseconds(200),
|
||||
tolerance: .nanoseconds(5),
|
||||
options: .nontrivialOptions),
|
||||
.now,
|
||||
.scheduleAfterDate(.nanoseconds(200),
|
||||
tolerance: .nanoseconds(5),
|
||||
options: .nontrivialOptions),
|
||||
.now,
|
||||
.scheduleAfterDate(.nanoseconds(200),
|
||||
tolerance: .nanoseconds(5),
|
||||
options: .nontrivialOptions)])
|
||||
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
helper.publisher.send(completion: .finished)
|
||||
XCTAssertEqual(helper.publisher.send(4), .none)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(delaySubscription),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(100)),
|
||||
.requested(.max(12)),
|
||||
.requested(.max(12)),
|
||||
.requested(.max(12))])
|
||||
XCTAssertEqual(scheduler.scheduledDates, [.nanoseconds(400)])
|
||||
XCTAssertEqual(scheduler.history,
|
||||
[.now,
|
||||
.scheduleAfterDate(.nanoseconds(200),
|
||||
tolerance: .nanoseconds(5),
|
||||
options: .nontrivialOptions),
|
||||
.now,
|
||||
.scheduleAfterDate(.nanoseconds(200),
|
||||
tolerance: .nanoseconds(5),
|
||||
options: .nontrivialOptions),
|
||||
.now,
|
||||
.scheduleAfterDate(.nanoseconds(200),
|
||||
tolerance: .nanoseconds(5),
|
||||
options: .nontrivialOptions),
|
||||
.now,
|
||||
.scheduleAfterDate(.nanoseconds(400),
|
||||
tolerance: .nanoseconds(5),
|
||||
options: .nontrivialOptions)])
|
||||
|
||||
scheduler.executeScheduledActions()
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(delaySubscription),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3),
|
||||
.completion(.failure(.oops))])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(100)),
|
||||
.requested(.max(12)),
|
||||
.requested(.max(12)),
|
||||
.requested(.max(12))])
|
||||
XCTAssertEqual(scheduler.history,
|
||||
[.now,
|
||||
.scheduleAfterDate(.nanoseconds(200),
|
||||
tolerance: .nanoseconds(5),
|
||||
options: .nontrivialOptions),
|
||||
.now,
|
||||
.scheduleAfterDate(.nanoseconds(200),
|
||||
tolerance: .nanoseconds(5),
|
||||
options: .nontrivialOptions),
|
||||
.now,
|
||||
.scheduleAfterDate(.nanoseconds(200),
|
||||
tolerance: .nanoseconds(5),
|
||||
options: .nontrivialOptions),
|
||||
.now,
|
||||
.scheduleAfterDate(.nanoseconds(400),
|
||||
tolerance: .nanoseconds(5),
|
||||
options: .nontrivialOptions)])
|
||||
XCTAssertEqual(scheduler.now, .nanoseconds(400))
|
||||
}
|
||||
|
||||
func testRequest() throws {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .none) {
|
||||
$0.delay(for: .nanoseconds(200),
|
||||
tolerance: .nanoseconds(5),
|
||||
scheduler: scheduler,
|
||||
options: .nontrivialOptions)
|
||||
}
|
||||
scheduler.executeScheduledActions()
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(delaySubscription)])
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(10))
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(4))
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(5))
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.none)
|
||||
XCTAssertEqual(helper.publisher.send(2000), .none)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(delaySubscription)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(10)),
|
||||
.requested(.max(4)),
|
||||
.requested(.max(5)),
|
||||
.requested(.none)])
|
||||
|
||||
scheduler.executeScheduledActions()
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(delaySubscription),
|
||||
.value(2000)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(10)),
|
||||
.requested(.max(4)),
|
||||
.requested(.max(5)),
|
||||
.requested(.none)])
|
||||
}
|
||||
|
||||
func testCancelAlreadyCancelled() throws {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .none) {
|
||||
$0.delay(for: .nanoseconds(200),
|
||||
tolerance: .nanoseconds(5),
|
||||
scheduler: scheduler,
|
||||
options: .nontrivialOptions)
|
||||
}
|
||||
|
||||
scheduler.executeScheduledActions()
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(delaySubscription)])
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(42))
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited), .cancelled])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(delaySubscription)])
|
||||
XCTAssertEqual(scheduler.history, [])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(0), .none)
|
||||
helper.publisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited), .cancelled])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(delaySubscription)])
|
||||
XCTAssertEqual(scheduler.history, [])
|
||||
}
|
||||
|
||||
func testReceiveCompletionImmediatelyAfterSubscription() {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .none) {
|
||||
$0.delay(for: .nanoseconds(123),
|
||||
tolerance: .nanoseconds(5),
|
||||
scheduler: scheduler,
|
||||
options: .nontrivialOptions)
|
||||
}
|
||||
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(delaySubscription)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(scheduler.history,
|
||||
[.now,
|
||||
.scheduleAfterDate(.nanoseconds(123),
|
||||
tolerance: .nanoseconds(5),
|
||||
options: .nontrivialOptions)])
|
||||
|
||||
scheduler.executeScheduledActions()
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(delaySubscription),
|
||||
.completion(.failure(.oops))])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
}
|
||||
|
||||
func testReceiveCompletionImmediatelyAfterValue() {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .max(418)) {
|
||||
$0.delay(for: .nanoseconds(123),
|
||||
tolerance: .nanoseconds(5),
|
||||
scheduler: scheduler,
|
||||
options: .nontrivialOptions)
|
||||
}
|
||||
XCTAssertEqual(helper.publisher.send(-1), .none)
|
||||
scheduler.executeScheduledActions()
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(1000), .none)
|
||||
helper.publisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(delaySubscription),
|
||||
.value(-1)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
|
||||
.requested(.max(418))])
|
||||
XCTAssertEqual(scheduler.history,
|
||||
[.now,
|
||||
.scheduleAfterDate(.nanoseconds(123),
|
||||
tolerance: .nanoseconds(5),
|
||||
options: .nontrivialOptions),
|
||||
.now,
|
||||
.scheduleAfterDate(.nanoseconds(246),
|
||||
tolerance: .nanoseconds(5),
|
||||
options: .nontrivialOptions),
|
||||
.now,
|
||||
.scheduleAfterDate(.nanoseconds(246),
|
||||
tolerance: .nanoseconds(5),
|
||||
options: .nontrivialOptions)])
|
||||
|
||||
scheduler.executeScheduledActions()
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(delaySubscription),
|
||||
.value(-1),
|
||||
.value(1000),
|
||||
.completion(.finished)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
|
||||
.requested(.max(418))])
|
||||
}
|
||||
|
||||
func testCrashesWhenReceivingInputRecursively() {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .max(418)) {
|
||||
$0.delay(for: .nanoseconds(123), scheduler: ImmediateScheduler.shared)
|
||||
}
|
||||
|
||||
var recursionCounter = 5
|
||||
helper.tracking.onValue = { _ in
|
||||
if recursionCounter == 0 { return }
|
||||
recursionCounter -= 1
|
||||
_ = helper.publisher.send(-1)
|
||||
}
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(0), .none)
|
||||
XCTAssertEqual(helper.tracking.history,
|
||||
[.subscription(delaySubscriptionImmediateScheduler),
|
||||
.value(0),
|
||||
.value(-1),
|
||||
.value(-1),
|
||||
.value(-1),
|
||||
.value(-1),
|
||||
.value(-1)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
|
||||
.requested(.max(418)),
|
||||
.requested(.max(418)),
|
||||
.requested(.max(418)),
|
||||
.requested(.max(418)),
|
||||
.requested(.max(418)),
|
||||
.requested(.max(418))])
|
||||
}
|
||||
|
||||
func testReceiveCompletionRecursively() {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .max(418)) {
|
||||
$0.delay(for: .nanoseconds(123), scheduler: ImmediateScheduler.shared)
|
||||
}
|
||||
helper.tracking.onFinish = {
|
||||
helper.publisher.send(completion: .finished)
|
||||
}
|
||||
helper.publisher.send(completion: .finished)
|
||||
}
|
||||
|
||||
func testStrongCaptureWhenSchedulingValue() {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
var value: Int?
|
||||
var subscriberReleased = false
|
||||
do {
|
||||
let publisher = CustomPublisher(subscription: CustomSubscription())
|
||||
let delay = publisher.delay(for: 0.35, scheduler: scheduler)
|
||||
let tracking = TrackingSubscriber(receiveValue: { value = $0; return .none },
|
||||
onDeinit: { subscriberReleased = true })
|
||||
delay.subscribe(tracking)
|
||||
scheduler.executeScheduledActions()
|
||||
XCTAssertEqual(tracking.history, [.subscription(delaySubscription)])
|
||||
XCTAssertEqual(publisher.send(42), .none)
|
||||
XCTAssertEqual(tracking.history, [.subscription(delaySubscription)])
|
||||
XCTAssertEqual(scheduler.history,
|
||||
[.minimumTolerance,
|
||||
.now,
|
||||
.scheduleAfterDate(.seconds(0.35),
|
||||
tolerance: 0,
|
||||
options: nil)])
|
||||
tracking.cancel()
|
||||
publisher.cancel()
|
||||
}
|
||||
XCTAssertFalse(subscriberReleased)
|
||||
scheduler.executeScheduledActions()
|
||||
XCTAssertEqual(value, 42)
|
||||
XCTAssertTrue(subscriberReleased)
|
||||
}
|
||||
|
||||
func testStrongCaptureWhenSchedulingCompletion() {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
var completion: Subscribers.Completion<TestingError>?
|
||||
var subscriberReleased = false
|
||||
do {
|
||||
let publisher = CustomPublisher(subscription: CustomSubscription())
|
||||
let delay = publisher.delay(for: 0.35, scheduler: scheduler)
|
||||
let tracking = TrackingSubscriber(receiveCompletion: { completion = $0 },
|
||||
onDeinit: { subscriberReleased = true })
|
||||
delay.subscribe(tracking)
|
||||
scheduler.executeScheduledActions()
|
||||
XCTAssertEqual(tracking.history, [.subscription(delaySubscription)])
|
||||
publisher.send(completion: .finished)
|
||||
XCTAssertEqual(tracking.history, [.subscription(delaySubscription)])
|
||||
XCTAssertEqual(scheduler.history,
|
||||
[.minimumTolerance,
|
||||
.now,
|
||||
.scheduleAfterDate(.seconds(0.35),
|
||||
tolerance: 0,
|
||||
options: nil)])
|
||||
tracking.cancel()
|
||||
publisher.cancel()
|
||||
}
|
||||
XCTAssertFalse(subscriberReleased)
|
||||
scheduler.executeScheduledActions()
|
||||
XCTAssertEqual(completion, .finished)
|
||||
XCTAssertTrue(subscriberReleased)
|
||||
}
|
||||
|
||||
func testDelayReceiveSubscriptionTwice() throws {
|
||||
try testReceiveSubscriptionTwice {
|
||||
$0.delay(for: 0.35, scheduler: ImmediateScheduler.shared)
|
||||
}
|
||||
}
|
||||
|
||||
func testDelayReceiveValueBeforeSubscription() {
|
||||
testReceiveValueBeforeSubscription(value: 213,
|
||||
expected: .history([], demand: .none)) {
|
||||
$0.delay(for: 0.35, scheduler: ImmediateScheduler.shared)
|
||||
}
|
||||
}
|
||||
|
||||
func testDelayReceiveCompletionBeforeSubscription() {
|
||||
testReceiveCompletionBeforeSubscription(inputType: Int.self,
|
||||
expected: .history([])) {
|
||||
$0.delay(for: 0.35, scheduler: ImmediateScheduler.shared)
|
||||
}
|
||||
}
|
||||
|
||||
func testDelayRequestBeforeSubscription() {
|
||||
testRequestBeforeSubscription(inputType: Int.self, shouldCrash: false) {
|
||||
$0.delay(for: 0.35, scheduler: ImmediateScheduler.shared)
|
||||
}
|
||||
}
|
||||
|
||||
func testDelayCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self, shouldCrash: false) {
|
||||
$0.delay(for: 0.35, scheduler: ImmediateScheduler.shared)
|
||||
}
|
||||
}
|
||||
|
||||
func testDelayReflection() throws {
|
||||
/// Delay's Inner doesn't customize its reflection
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: Error.self,
|
||||
description: nil,
|
||||
customMirror: nil,
|
||||
playgroundDescription: nil) {
|
||||
$0.delay(for: 42, scheduler: ImmediateScheduler.shared)
|
||||
}
|
||||
}
|
||||
|
||||
func testDelayLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: true) {
|
||||
$0.delay(for: 42, scheduler: ImmediateScheduler.shared)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,359 @@
|
||||
//
|
||||
// DropUntilOutputTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 24.12.2019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class DropUntilOutputTests: XCTestCase {
|
||||
|
||||
func testOtherCompletesBeforeTriggering() {
|
||||
let otherSubscription = CustomSubscription()
|
||||
let otherPublisher = CustomPublisher(subscription: otherSubscription)
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(100),
|
||||
receiveValueDemand: .max(5),
|
||||
createSut: { $0.drop(untilOutputFrom: otherPublisher) }
|
||||
)
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(1), .none)
|
||||
XCTAssertEqual(helper.publisher.send(2), .none)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("DropUntilOutput")])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(100))])
|
||||
XCTAssertEqual(otherSubscription.history, [.requested(.max(1))])
|
||||
|
||||
otherPublisher.send(completion: .finished)
|
||||
otherPublisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("DropUntilOutput"),
|
||||
.completion(.finished),
|
||||
.completion(.finished)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(100)), .cancelled])
|
||||
XCTAssertEqual(otherSubscription.history, [.requested(.max(1))])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(3), .none)
|
||||
XCTAssertEqual(otherPublisher.send(1000), .none)
|
||||
XCTAssertEqual(helper.publisher.send(4), .max(5))
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("DropUntilOutput"),
|
||||
.completion(.finished),
|
||||
.completion(.finished),
|
||||
.value(4)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(100)), .cancelled])
|
||||
XCTAssertEqual(otherSubscription.history, [.requested(.max(1))])
|
||||
}
|
||||
|
||||
func testOtherFailsAfterTriggering() {
|
||||
let otherSubscription = CustomSubscription()
|
||||
let otherPublisher = CustomPublisher(subscription: otherSubscription)
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(2),
|
||||
receiveValueDemand: .max(5),
|
||||
createSut: { $0.drop(untilOutputFrom: otherPublisher) }
|
||||
)
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(1), .none)
|
||||
XCTAssertEqual(otherPublisher.send(1000), .none)
|
||||
XCTAssertEqual(helper.publisher.send(2), .max(5))
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("DropUntilOutput"),
|
||||
.value(2)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(2))])
|
||||
XCTAssertEqual(otherSubscription.history, [.requested(.max(1))])
|
||||
|
||||
otherPublisher.send(completion: .failure(.oops))
|
||||
otherPublisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("DropUntilOutput"),
|
||||
.value(2)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(2))])
|
||||
XCTAssertEqual(otherSubscription.history, [.requested(.max(1))])
|
||||
}
|
||||
|
||||
func testDemand() throws {
|
||||
let subscription = CustomSubscription()
|
||||
let otherSubscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let otherPublisher = CustomPublisher(subscription: otherSubscription)
|
||||
let dropUntilOutput = publisher.drop(untilOutputFrom: otherPublisher)
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: { subscription in
|
||||
downstreamSubscription = subscription
|
||||
},
|
||||
receiveValue: { .max($0) }
|
||||
)
|
||||
dropUntilOutput.subscribe(tracking)
|
||||
|
||||
XCTAssertEqual(subscription.history, [])
|
||||
XCTAssertEqual(otherSubscription.history, [.requested(.max(1))])
|
||||
XCTAssertEqual(tracking.history, [.subscription("DropUntilOutput")])
|
||||
|
||||
try XCTUnwrap(downstreamSubscription).request(.max(4))
|
||||
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(4))])
|
||||
XCTAssertEqual(otherSubscription.history, [.requested(.max(1))])
|
||||
XCTAssertEqual(tracking.history, [.subscription("DropUntilOutput")])
|
||||
|
||||
XCTAssertEqual(publisher.send(1), .none)
|
||||
XCTAssertEqual(publisher.send(2), .none)
|
||||
XCTAssertEqual(otherPublisher.send(1000), .none)
|
||||
XCTAssertEqual(publisher.send(3), .max(3))
|
||||
XCTAssertEqual(publisher.send(4), .max(4))
|
||||
XCTAssertEqual(publisher.send(5), .max(5))
|
||||
XCTAssertEqual(publisher.send(6), .max(6))
|
||||
XCTAssertEqual(publisher.send(7), .max(7))
|
||||
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(4))])
|
||||
XCTAssertEqual(otherSubscription.history, [.requested(.max(1))])
|
||||
XCTAssertEqual(tracking.history, [.subscription("DropUntilOutput"),
|
||||
.value(3),
|
||||
.value(4),
|
||||
.value(5),
|
||||
.value(6),
|
||||
.value(7)])
|
||||
}
|
||||
|
||||
func testCancelAlreadyCancelled() throws {
|
||||
let otherSubscription = CustomSubscription()
|
||||
let otherPublisher = CustomPublisher(subscription: otherSubscription)
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(2),
|
||||
receiveValueDemand: .max(5),
|
||||
createSut: { $0.drop(untilOutputFrom: otherPublisher) }
|
||||
)
|
||||
|
||||
helper.subscription.onCancel = {
|
||||
XCTAssertEqual(otherSubscription.history, [.requested(.max(1))])
|
||||
}
|
||||
|
||||
otherSubscription.onCancel = {
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(2)), .cancelled])
|
||||
}
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(10))
|
||||
XCTAssertEqual(helper.publisher.send(1000), .none)
|
||||
helper.publisher.send(completion: .finished)
|
||||
|
||||
let subscription2 = CustomSubscription()
|
||||
helper.publisher.send(subscription: subscription2)
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(2)), .cancelled])
|
||||
XCTAssertEqual(otherSubscription.history, [.requested(.max(1)), .cancelled])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("DropUntilOutput")])
|
||||
XCTAssertEqual(subscription2.history, [.cancelled])
|
||||
}
|
||||
|
||||
func testSubscribesToOtherFirst() {
|
||||
let subscription = CustomSubscription()
|
||||
let otherSubscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let otherPublisher = CustomPublisher(subscription: otherSubscription)
|
||||
let dropUntilOutput = publisher.drop(untilOutputFrom: otherPublisher)
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: { _ in
|
||||
XCTAssertNotNil(publisher.subscriber)
|
||||
XCTAssertNotNil(otherPublisher.subscriber)
|
||||
XCTAssertEqual(subscription.history, [])
|
||||
XCTAssertEqual(otherSubscription.history, [.requested(.max(1))])
|
||||
}
|
||||
)
|
||||
|
||||
otherPublisher.willSubscribe = { _ in
|
||||
XCTAssertNil(publisher.subscriber)
|
||||
}
|
||||
|
||||
publisher.willSubscribe = { _ in
|
||||
XCTAssertNotNil(otherPublisher.subscriber)
|
||||
}
|
||||
|
||||
dropUntilOutput.subscribe(tracking)
|
||||
tracking.cancel()
|
||||
}
|
||||
|
||||
func testSubscribersHaveTheSameCombineIdentifier() {
|
||||
let subscription = CustomSubscription()
|
||||
let otherSubscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let otherPublisher = CustomPublisher(subscription: otherSubscription)
|
||||
let dropUntilOutput = publisher.drop(untilOutputFrom: otherPublisher)
|
||||
let tracking = TrackingSubscriber()
|
||||
dropUntilOutput.subscribe(tracking)
|
||||
|
||||
XCTAssert(publisher.erasedSubscriber is CustomCombineIdentifierConvertible)
|
||||
XCTAssert(otherPublisher.erasedSubscriber is CustomCombineIdentifierConvertible)
|
||||
XCTAssertEqual(
|
||||
(publisher.erasedSubscriber as? CustomCombineIdentifierConvertible)?
|
||||
.combineIdentifier,
|
||||
(otherPublisher.erasedSubscriber as? CustomCombineIdentifierConvertible)?
|
||||
.combineIdentifier
|
||||
)
|
||||
}
|
||||
|
||||
func testLateSubscription() throws {
|
||||
|
||||
// This publisher doesn't send a subscription when it receives a subscriber
|
||||
let publisher = CustomPublisher(subscription: nil)
|
||||
let dropUntilOutput = publisher.drop(untilOutputFrom: Empty<Void, TestingError>())
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: {
|
||||
$0.request(.max(10))
|
||||
$0.request(.max(4))
|
||||
$0.request(.none)
|
||||
}
|
||||
)
|
||||
|
||||
dropUntilOutput.subscribe(tracking)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.completion(.finished),
|
||||
.subscription("DropUntilOutput")])
|
||||
|
||||
let subscription = CustomSubscription()
|
||||
try XCTUnwrap(publisher.subscriber).receive(subscription: subscription)
|
||||
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(14))])
|
||||
XCTAssertEqual(tracking.history, [.completion(.finished),
|
||||
.subscription("DropUntilOutput")])
|
||||
}
|
||||
|
||||
func testReusableOtherSubscriber() throws {
|
||||
let otherSubscription = CustomSubscription()
|
||||
let otherPublisher = CustomPublisher(subscription: otherSubscription)
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(2),
|
||||
receiveValueDemand: .max(5),
|
||||
createSut: { $0.drop(untilOutputFrom: otherPublisher) }
|
||||
)
|
||||
|
||||
let subscription2 = CustomSubscription()
|
||||
try XCTUnwrap(otherPublisher.subscriber).receive(subscription: subscription2)
|
||||
|
||||
XCTAssertEqual(subscription2.history, [.cancelled])
|
||||
XCTAssertEqual(otherPublisher.send(1000), .none)
|
||||
|
||||
let subscription3 = CustomSubscription()
|
||||
try XCTUnwrap(otherPublisher.subscriber).receive(subscription: subscription3)
|
||||
|
||||
XCTAssertEqual(subscription3.history, [.requested(.max(1))])
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
|
||||
XCTAssertEqual(subscription3.history, [.requested(.max(1)),
|
||||
.cancelled])
|
||||
XCTAssertEqual(otherSubscription.history, [.requested(.max(1))])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(2)), .cancelled])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("DropUntilOutput")])
|
||||
}
|
||||
|
||||
func testCrashesWhenReceivesInputAfterCancel() {
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .none,
|
||||
createSut: { $0.drop(untilOutputFrom: Empty<Void, TestingError>()) }
|
||||
)
|
||||
|
||||
assertCrashes {
|
||||
_ = helper.publisher.send(0)
|
||||
}
|
||||
}
|
||||
|
||||
func testDropUntilOutputReceiveValueBeforeSubscription() {
|
||||
testReceiveValueBeforeSubscription(
|
||||
value: 42,
|
||||
expected: .crash,
|
||||
{ $0.drop(untilOutputFrom: Empty<Int, Never>()) }
|
||||
)
|
||||
}
|
||||
|
||||
func testDropUntilOutputOtherReceiveValueBeforeSubscription() {
|
||||
testReceiveValueBeforeSubscription(
|
||||
value: 42,
|
||||
expected: .history([.completion(.finished), .subscription("DropUntilOutput")],
|
||||
demand: .none),
|
||||
{ Empty<Int, Never>().drop(untilOutputFrom: $0) }
|
||||
)
|
||||
}
|
||||
|
||||
func testDropUntilOutputReceiveCompletionBeforeSubscription() {
|
||||
testReceiveCompletionBeforeSubscription(
|
||||
inputType: Int.self,
|
||||
expected: .history([.completion(.finished),
|
||||
.subscription("DropUntilOutput"),
|
||||
.completion(.finished)]),
|
||||
{ $0.drop(untilOutputFrom: Empty<Int, Never>()) }
|
||||
)
|
||||
}
|
||||
|
||||
func testDropUntilOutputOtherReceiveCompletionBeforeSubscription() {
|
||||
testReceiveCompletionBeforeSubscription(
|
||||
inputType: Int.self,
|
||||
expected: .history([.completion(.finished),
|
||||
.subscription("DropUntilOutput"),
|
||||
.completion(.finished)]),
|
||||
{ Empty<Int, Never>().drop(untilOutputFrom: $0) }
|
||||
)
|
||||
}
|
||||
|
||||
func testDropUntilOutputRequestBeforeSubscription() {
|
||||
testRequestBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
{ $0.drop(untilOutputFrom: Empty<Int, Never>()) })
|
||||
}
|
||||
|
||||
func testDropUntilOutputCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
{ $0.drop(untilOutputFrom: Empty<Int, Never>()) })
|
||||
}
|
||||
|
||||
func testDropUntilOutputReceiveSubscriptionTwice() throws {
|
||||
try testReceiveSubscriptionTwice {
|
||||
$0.drop(untilOutputFrom: Empty<Int, TestingError>())
|
||||
}
|
||||
}
|
||||
|
||||
func testDropUntilOutputLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
{ $0.drop(untilOutputFrom: Empty<Int, TestingError>()) })
|
||||
}
|
||||
|
||||
func testDropUntilOutputOtherLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
{ Empty<Int, TestingError>().drop(untilOutputFrom: $0) })
|
||||
}
|
||||
|
||||
func testDropUntilOutputReflection() throws {
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: TestingError.self,
|
||||
description: "DropUntilOutput",
|
||||
customMirror: childrenIsEmpty,
|
||||
playgroundDescription: "DropUntilOutput",
|
||||
{ $0.drop(untilOutputFrom: Empty<Int, TestingError>()) })
|
||||
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: TestingError.self,
|
||||
description: "DropUntilOutput",
|
||||
customMirror: childrenIsEmpty,
|
||||
playgroundDescription: "DropUntilOutput",
|
||||
{ Empty<Int, TestingError>().drop(untilOutputFrom: $0) })
|
||||
}
|
||||
}
|
||||
@@ -75,7 +75,7 @@ final class FlatMapTests: XCTestCase {
|
||||
XCTAssertNil(upstream.erasedSubscriber)
|
||||
}
|
||||
)
|
||||
upstream.onSubscribe = { _ in
|
||||
upstream.willSubscribe = { _ in
|
||||
upstreamReceivedSubscriber = true
|
||||
XCTAssertEqual(tracking.history, [.subscription("FlatMap")])
|
||||
}
|
||||
@@ -390,11 +390,15 @@ final class FlatMapTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testChildValueReceivedWhileSendingValue() throws {
|
||||
let upstreamPublisher = PassthroughSubject<AnyPublisher<Int, TestingError>,
|
||||
TestingError>()
|
||||
let upstreamSubscription = CustomSubscription()
|
||||
let upstreamPublisher = CustomPublisherBase<CustomPublisher, TestingError>(
|
||||
subscription: upstreamSubscription
|
||||
)
|
||||
|
||||
let child1Publisher = CustomPublisher(subscription: CustomSubscription())
|
||||
let child2Publisher = CustomPublisher(subscription: CustomSubscription())
|
||||
let childSubscription1 = CustomSubscription()
|
||||
let childSubscription2 = CustomSubscription()
|
||||
let child1Publisher = CustomPublisher(subscription: childSubscription1)
|
||||
let child2Publisher = CustomPublisher(subscription: childSubscription2)
|
||||
|
||||
let flatMap = upstreamPublisher.flatMap { $0 }
|
||||
|
||||
@@ -408,12 +412,17 @@ final class FlatMapTests: XCTestCase {
|
||||
|
||||
flatMap.subscribe(downstreamSubscriber)
|
||||
|
||||
upstreamPublisher.send(AnyPublisher(child1Publisher))
|
||||
upstreamPublisher.send(AnyPublisher(child2Publisher))
|
||||
XCTAssertEqual(upstreamPublisher.send(child1Publisher), .none)
|
||||
XCTAssertEqual(upstreamPublisher.send(child2Publisher), .none)
|
||||
|
||||
assertCrashes {
|
||||
XCTAssertEqual(child1Publisher.send(666), .max(1))
|
||||
}
|
||||
XCTAssertEqual(child1Publisher.send(666), .max(1))
|
||||
|
||||
XCTAssertEqual(upstreamSubscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(downstreamSubscriber.history, [.subscription("FlatMap"),
|
||||
.value(666),
|
||||
.value(777)])
|
||||
XCTAssertEqual(childSubscription1.history, [.requested(.max(1))])
|
||||
XCTAssertEqual(childSubscription2.history, [.requested(.max(1))])
|
||||
}
|
||||
|
||||
func testOuterLockReentrance() {
|
||||
@@ -462,9 +471,6 @@ final class FlatMapTests: XCTestCase {
|
||||
// Create some downstream demand
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(5))
|
||||
|
||||
// If Apple changes the implementation to use recursive lock,
|
||||
// we must make sure no stack overflow occurs here,
|
||||
// which will also be detected as a crash, which is not what we want.
|
||||
var recursionDepth = 10
|
||||
helper.tracking.onFailure = { _ in
|
||||
if recursionDepth <= 0 {
|
||||
@@ -474,10 +480,13 @@ final class FlatMapTests: XCTestCase {
|
||||
_ = child.send(1)
|
||||
}
|
||||
|
||||
// Expected deadlock
|
||||
assertCrashes {
|
||||
child.send(completion: .failure(.oops))
|
||||
}
|
||||
child.send(completion: .failure(.oops))
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("FlatMap"),
|
||||
.completion(.failure(.oops)),
|
||||
.value(1)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(1))])
|
||||
XCTAssertEqual(childSubscription.history, [.requested(.max(1))])
|
||||
}
|
||||
|
||||
func testCompletesProperlyWhenUpstreamOutlivesChildren() {
|
||||
@@ -564,7 +573,7 @@ final class FlatMapTests: XCTestCase {
|
||||
createSut: { $0.flatMap(maxPublishers: .max(1)) { $0 } }
|
||||
)
|
||||
|
||||
child.onSubscribe = { subscriber in
|
||||
child.willSubscribe = { subscriber in
|
||||
helper.publisher.send(completion: .finished)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
//
|
||||
// HandleEventsTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 03.12.2019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class HandleEventsTests: XCTestCase {
|
||||
|
||||
func testBasicBehavior() throws {
|
||||
var history = [Event<TestingError>]()
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let handleEvents = publisher.handleAllEvents { history.append($0) }
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: {
|
||||
XCTAssertNil(publisher.subscriber)
|
||||
downstreamSubscription = $0
|
||||
},
|
||||
receiveValue: { .max($0) }
|
||||
)
|
||||
handleEvents.subscribe(tracking)
|
||||
|
||||
XCTAssertNotNil(publisher.subscriber)
|
||||
XCTAssertEqual(tracking.history, [.subscription("HandleEvents")])
|
||||
XCTAssertEqual(subscription.history, [])
|
||||
XCTAssertEqual(history, [.receiveSubscription("CustomSubscription")])
|
||||
|
||||
XCTAssertEqual(publisher.send(0), .none)
|
||||
XCTAssertEqual(publisher.send(1), .max(1))
|
||||
XCTAssertEqual(publisher.send(2), .max(2))
|
||||
XCTAssertEqual(publisher.send(3), .max(3))
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("HandleEvents"),
|
||||
.value(0),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3)])
|
||||
XCTAssertEqual(subscription.history, [])
|
||||
XCTAssertEqual(history, [.receiveSubscription("CustomSubscription"),
|
||||
.receiveOutput(0),
|
||||
.receiveOutput(1),
|
||||
.receiveRequest(.max(1)),
|
||||
.receiveOutput(2),
|
||||
.receiveRequest(.max(2)),
|
||||
.receiveOutput(3),
|
||||
.receiveRequest(.max(3))])
|
||||
|
||||
try XCTUnwrap(downstreamSubscription).request(.max(14))
|
||||
try XCTUnwrap(downstreamSubscription).request(.max(10))
|
||||
try XCTUnwrap(downstreamSubscription).request(.none)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("HandleEvents"),
|
||||
.value(0),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3)])
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(14)),
|
||||
.requested(.max(10)),
|
||||
.requested(.none)])
|
||||
XCTAssertEqual(history, [.receiveSubscription("CustomSubscription"),
|
||||
.receiveOutput(0),
|
||||
.receiveOutput(1),
|
||||
.receiveRequest(.max(1)),
|
||||
.receiveOutput(2),
|
||||
.receiveRequest(.max(2)),
|
||||
.receiveOutput(3),
|
||||
.receiveRequest(.max(3)),
|
||||
.receiveRequest(.max(14)),
|
||||
.receiveRequest(.max(10)),
|
||||
.receiveRequest(.none)])
|
||||
|
||||
publisher.send(completion: .finished)
|
||||
publisher.send(completion: .failure(.oops))
|
||||
publisher.send(completion: .finished)
|
||||
XCTAssertEqual(publisher.send(144), .max(144))
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("HandleEvents"),
|
||||
.value(0),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3),
|
||||
.completion(.finished),
|
||||
.completion(.failure(.oops)),
|
||||
.completion(.finished),
|
||||
.value(144)])
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(14)),
|
||||
.requested(.max(10)),
|
||||
.requested(.none)])
|
||||
XCTAssertEqual(history, [.receiveSubscription("CustomSubscription"),
|
||||
.receiveOutput(0),
|
||||
.receiveOutput(1),
|
||||
.receiveRequest(.max(1)),
|
||||
.receiveOutput(2),
|
||||
.receiveRequest(.max(2)),
|
||||
.receiveOutput(3),
|
||||
.receiveRequest(.max(3)),
|
||||
.receiveRequest(.max(14)),
|
||||
.receiveRequest(.max(10)),
|
||||
.receiveRequest(.none),
|
||||
.receiveCompletion(.finished)])
|
||||
}
|
||||
|
||||
func testAccumulatesDemandUntilSubscriptionArrives() {
|
||||
var history = [Event<TestingError>]()
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let handleEvents = publisher.handleAllEvents { history.append($0) }
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: {
|
||||
XCTAssertNil(publisher.subscriber)
|
||||
XCTAssertEqual(history, [])
|
||||
XCTAssertEqual(subscription.history, [])
|
||||
$0.request(.max(45))
|
||||
$0.request(.none)
|
||||
$0.request(.max(13))
|
||||
XCTAssertEqual(subscription.history, [])
|
||||
downstreamSubscription = $0
|
||||
},
|
||||
receiveValue: { .max($0) }
|
||||
)
|
||||
handleEvents.subscribe(tracking)
|
||||
XCTAssertEqual(tracking.history, [.subscription("HandleEvents")])
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(58))])
|
||||
XCTAssertEqual(history, [.receiveRequest(.max(45)),
|
||||
.receiveRequest(.none),
|
||||
.receiveRequest(.max(13)),
|
||||
.receiveSubscription("CustomSubscription")])
|
||||
XCTAssertNotNil(downstreamSubscription)
|
||||
}
|
||||
|
||||
func testCancelAlreadyCancelled() throws {
|
||||
var history = [Event<TestingError>]()
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(2),
|
||||
receiveValueDemand: .max(5)) {
|
||||
$0.handleAllEvents { history.append($0) }
|
||||
}
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("HandleEvents")])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(2))])
|
||||
XCTAssertEqual(history, [.receiveRequest(.max(2)),
|
||||
.receiveSubscription("CustomSubscription")])
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(1))
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(1), .max(5))
|
||||
helper.publisher.send(completion: .finished)
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("HandleEvents"),
|
||||
.value(1),
|
||||
.completion(.finished),
|
||||
.completion(.failure(.oops))])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(2)),
|
||||
.cancelled])
|
||||
XCTAssertEqual(history, [.receiveRequest(.max(2)),
|
||||
.receiveSubscription("CustomSubscription"),
|
||||
.receiveCancel])
|
||||
}
|
||||
|
||||
func testHandleEventsReceiveSubscriptionTwice() throws {
|
||||
var history = [Event<TestingError>]()
|
||||
try testReceiveSubscriptionTwice { $0.handleAllEvents { history.append($0) } }
|
||||
XCTAssertEqual(history, [.receiveSubscription("CustomSubscription"),
|
||||
.receiveSubscription("CustomSubscription"),
|
||||
.receiveSubscription("CustomSubscription"),
|
||||
.receiveCancel])
|
||||
}
|
||||
|
||||
func testHandleEventsReceiveValueBeforeSubscription() {
|
||||
var history = [Event<Never>]()
|
||||
testReceiveValueBeforeSubscription(
|
||||
value: 144,
|
||||
expected: .history([.subscription("HandleEvents"),
|
||||
.value(144)],
|
||||
demand: .max(42)),
|
||||
{ $0.handleAllEvents { history.append($0) } }
|
||||
)
|
||||
XCTAssertEqual(history, [.receiveOutput(144),
|
||||
.receiveRequest(.max(42))])
|
||||
}
|
||||
|
||||
func testHandleEventsReceiveCompletionBeforeSubscription() {
|
||||
var history = [Event<Never>]()
|
||||
testReceiveCompletionBeforeSubscription(
|
||||
inputType: Int.self,
|
||||
expected: .history([.subscription("HandleEvents"), .completion(.finished)]),
|
||||
{ $0.handleAllEvents { history.append($0) } }
|
||||
)
|
||||
XCTAssertEqual(history, [.receiveCompletion(.finished)])
|
||||
}
|
||||
|
||||
func testHandleEventsRequestBeforeSubscription() {
|
||||
var history = [Event<Never>]()
|
||||
testRequestBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
{ $0.handleAllEvents { history.append($0) } })
|
||||
XCTAssertEqual(history, [.receiveRequest(.max(1))])
|
||||
}
|
||||
|
||||
func testHandleEventsCancelBeforeSubscription() {
|
||||
var history = [Event<Never>]()
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
{ $0.handleAllEvents { history.append($0) } })
|
||||
XCTAssertEqual(history, [.receiveCancel])
|
||||
}
|
||||
|
||||
func testHandleEventsReflection() throws {
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: Error.self,
|
||||
description: "HandleEvents",
|
||||
customMirror: childrenIsEmpty,
|
||||
playgroundDescription: "HandleEvents",
|
||||
{ $0.handleEvents() })
|
||||
}
|
||||
|
||||
func testHandleEventsLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
{ $0.handleEvents() })
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private enum Event<Failure: Error & Equatable>: Equatable {
|
||||
case receiveSubscription(StringSubscription)
|
||||
case receiveOutput(Int)
|
||||
case receiveCompletion(Subscribers.Completion<Failure>)
|
||||
case receiveCancel
|
||||
case receiveRequest(Subscribers.Demand)
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
extension Publisher where Output == Int, Failure: Equatable {
|
||||
fileprivate func handleAllEvents(
|
||||
_ handle: @escaping (Event<Failure>) -> Void
|
||||
) -> Publishers.HandleEvents<Self> {
|
||||
return handleEvents(
|
||||
receiveSubscription: { handle(.receiveSubscription(.subscription($0))) },
|
||||
receiveOutput: { handle(.receiveOutput($0)) },
|
||||
receiveCompletion: { handle(.receiveCompletion($0)) },
|
||||
receiveCancel: { handle(.receiveCancel) },
|
||||
receiveRequest: { handle(.receiveRequest($0)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -339,4 +339,29 @@ final class JustTests: XCTestCase {
|
||||
func testSetFailureTypeOperatorSpecialization() {
|
||||
XCTAssertEqual(try Sut(73).setFailureType(to: TestingError.self).result.get(), 73)
|
||||
}
|
||||
|
||||
func testPrependOperatorSpecialization() {
|
||||
XCTAssertEqual(Sut<Int>(5).prepend(1, 2, 3, 4), .init(sequence: [1, 2, 3, 4, 5]))
|
||||
|
||||
let trackingCollection = TrackingCollection<Int>([1, 2, 3, 4])
|
||||
XCTAssertEqual(Sut<Int>(5).prepend(trackingCollection),
|
||||
.init(sequence: [1, 2, 3, 4, 5]))
|
||||
|
||||
XCTAssertEqual(trackingCollection.history, [.initFromSequence,
|
||||
.underestimatedCount,
|
||||
.underestimatedCount,
|
||||
.makeIterator])
|
||||
}
|
||||
|
||||
func testAppendOperatorSpecialization() {
|
||||
XCTAssertEqual(Sut<Int>(1).append(2, 3, 4, 5), .init(sequence: [1, 2, 3, 4, 5]))
|
||||
|
||||
let trackingCollection = TrackingCollection<Int>([2, 3, 4, 5])
|
||||
XCTAssertEqual(Sut<Int>(1).append(trackingCollection),
|
||||
.init(sequence: [1, 2, 3, 4, 5]))
|
||||
|
||||
XCTAssertEqual(trackingCollection.history, [.initFromSequence,
|
||||
.underestimatedCount,
|
||||
.makeIterator])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,10 +210,15 @@ final class MapErrorTests: XCTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
private struct OtherError: Error {
|
||||
let original: Error
|
||||
private struct OtherError: EquatableError {
|
||||
let original: EquatableError
|
||||
|
||||
init(_ original: Error) {
|
||||
init(_ original: EquatableError) {
|
||||
self.original = original
|
||||
}
|
||||
|
||||
func isEqual(_ other: EquatableError) -> Bool {
|
||||
guard let other = other as? OtherError else { return false }
|
||||
return original.isEqual(other.original)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
//
|
||||
// MeasureIntervalTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 03.12.2019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class MeasureIntervalTests: XCTestCase {
|
||||
|
||||
func testBasicBehavior() {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
scheduler.rewind(to: .nanoseconds(3))
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(13),
|
||||
receiveValueDemand: .none) {
|
||||
$0.measureInterval(using: scheduler, options: .nontrivialOptions)
|
||||
}
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("MeasureInterval")])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(13))])
|
||||
XCTAssertEqual(scheduler.history, [.now])
|
||||
|
||||
scheduler.rewind(to: .nanoseconds(14))
|
||||
XCTAssertEqual(helper.publisher.send(1), .none)
|
||||
scheduler.rewind(to: .nanoseconds(17))
|
||||
XCTAssertEqual(helper.publisher.send(2), .none)
|
||||
scheduler.rewind(to: .nanoseconds(10))
|
||||
XCTAssertEqual(helper.publisher.send(3), .none)
|
||||
scheduler.rewind(to: .nanoseconds(21))
|
||||
XCTAssertEqual(helper.publisher.send(4), .none)
|
||||
helper.publisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("MeasureInterval"),
|
||||
.value(.nanoseconds(11)),
|
||||
.value(.nanoseconds(3)),
|
||||
.value(.nanoseconds(-7)),
|
||||
.value(.nanoseconds(11)),
|
||||
.completion(.finished)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(13))])
|
||||
XCTAssertEqual(scheduler.history, [.now, .now, .now, .now, .now])
|
||||
}
|
||||
|
||||
func testRequest() throws {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .max(1)) {
|
||||
$0.measureInterval(using: scheduler, options: .nontrivialOptions)
|
||||
}
|
||||
|
||||
var recursionCounter = 5
|
||||
helper.subscription.onRequest = { _ in
|
||||
if recursionCounter == 0 { return }
|
||||
recursionCounter -= 1
|
||||
XCTAssertEqual(helper.publisher.send(0), .none)
|
||||
}
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(0), .none)
|
||||
|
||||
helper.subscription.onRequest = nil
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(2))
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.unlimited)
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.none)
|
||||
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("MeasureInterval"),
|
||||
.value(.zero),
|
||||
.value(.zero),
|
||||
.value(.zero),
|
||||
.value(.zero),
|
||||
.value(.zero),
|
||||
.value(.zero),
|
||||
.completion(.failure(.oops))])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(1)),
|
||||
.requested(.max(1)),
|
||||
.requested(.max(1)),
|
||||
.requested(.max(1)),
|
||||
.requested(.max(1)),
|
||||
.requested(.max(1)),
|
||||
.requested(.max(2)),
|
||||
.requested(.unlimited),
|
||||
.requested(.none)])
|
||||
}
|
||||
|
||||
func testCancelAlreadyCancelled() throws {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .max(1)) {
|
||||
$0.measureInterval(using: scheduler, options: .nontrivialOptions)
|
||||
}
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("MeasureInterval")])
|
||||
XCTAssertEqual(helper.subscription.history, [])
|
||||
XCTAssertEqual(scheduler.history, [.now])
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(1000))
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
XCTAssertEqual(helper.publisher.send(1000), .none)
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("MeasureInterval")])
|
||||
XCTAssertEqual(helper.subscription.history, [.cancelled])
|
||||
XCTAssertEqual(scheduler.history, [.now])
|
||||
}
|
||||
|
||||
func testMeasureIntervalReceiveSubscriptionTwice() throws {
|
||||
try testReceiveSubscriptionTwice {
|
||||
$0.measureInterval(using: ImmediateScheduler.shared)
|
||||
}
|
||||
}
|
||||
|
||||
func testMeasureIntervalReceiveValueBeforeSubscription() {
|
||||
testReceiveValueBeforeSubscription(value: 213,
|
||||
expected: .history([], demand: .none)) {
|
||||
$0.measureInterval(using: ImmediateScheduler.shared)
|
||||
}
|
||||
}
|
||||
|
||||
func testMeasureIntervalReceiveCompletionBeforeSubscription() {
|
||||
testReceiveCompletionBeforeSubscription(inputType: Int.self,
|
||||
expected: .history([])) {
|
||||
$0.measureInterval(using: ImmediateScheduler.shared)
|
||||
}
|
||||
}
|
||||
|
||||
func testMeasureIntervalRequestBeforeSubscription() {
|
||||
testRequestBeforeSubscription(inputType: Int.self, shouldCrash: false) {
|
||||
$0.measureInterval(using: ImmediateScheduler.shared)
|
||||
}
|
||||
}
|
||||
|
||||
func testMeasureIntervalCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self, shouldCrash: false) {
|
||||
$0.measureInterval(using: ImmediateScheduler.shared)
|
||||
}
|
||||
}
|
||||
|
||||
func testMeasureIntervalReflection() throws {
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: TestingError.self,
|
||||
description: "MeasureInterval",
|
||||
customMirror: childrenIsEmpty,
|
||||
playgroundDescription: "MeasureInterval",
|
||||
{ $0.measureInterval(using: ImmediateScheduler.shared) })
|
||||
}
|
||||
|
||||
func testMeasureIntervalLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: true,
|
||||
{ $0.measureInterval(using: ImmediateScheduler.shared) })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,452 @@
|
||||
//
|
||||
// MergeTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 06.01.2020.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class MergeTests: XCTestCase {
|
||||
|
||||
private func createTestPublishers(
|
||||
arity: Int
|
||||
) -> ([CustomSubscription], [CustomPublisher]) {
|
||||
precondition(arity >= 0)
|
||||
let subscriptions = (0 ..< arity).map { _ in
|
||||
CustomSubscription()
|
||||
}
|
||||
let publishers = (0 ..< arity).map {
|
||||
CustomPublisher(subscription: subscriptions[$0])
|
||||
}
|
||||
return (subscriptions, publishers)
|
||||
}
|
||||
|
||||
func testMergeLimitedInitialDemand() {
|
||||
func test<Merger: Publisher>(
|
||||
forArity arity: Int,
|
||||
_ makeMerger: ([CustomPublisher]) -> Merger
|
||||
) where Merger.Output == Int, Merger.Failure == TestingError {
|
||||
let (subscriptions, publishers) = createTestPublishers(arity: arity)
|
||||
let merger = makeMerger(publishers)
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: { downstreamSubscription = $0 },
|
||||
receiveValue: { _ in .max(3) }
|
||||
)
|
||||
merger.subscribe(tracking)
|
||||
|
||||
for (i, subscription) in subscriptions.enumerated() {
|
||||
XCTAssertEqual(subscription.history,
|
||||
[.requested(.max(1))],
|
||||
"failure for subscription \(i)")
|
||||
}
|
||||
XCTAssertEqual(tracking.history, [.subscription("Merge")])
|
||||
|
||||
// No downstream demand, these values are buffered
|
||||
for (i, publisher) in publishers.reversed().enumerated() {
|
||||
XCTAssertEqual(publisher.send(-i), .none) // ignored
|
||||
XCTAssertEqual(publisher.send(i), .none)
|
||||
}
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Merge")])
|
||||
|
||||
// Establishing downstream demand
|
||||
downstreamSubscription?.request(.max(arity))
|
||||
downstreamSubscription?.request(.max(arity))
|
||||
|
||||
for (i, subscription) in subscriptions.enumerated() {
|
||||
XCTAssertEqual(subscription.history,
|
||||
[.requested(.max(1)),
|
||||
.requested(.max(1))],
|
||||
"failure for subscription \(i)")
|
||||
}
|
||||
let expectedValues: [TrackingSubscriber.Event] = (0 ..< arity)
|
||||
.reversed()
|
||||
.map {
|
||||
.value($0)
|
||||
}
|
||||
XCTAssertEqual(tracking.history,
|
||||
[.subscription("Merge")] + expectedValues)
|
||||
|
||||
// Requesting more elements
|
||||
downstreamSubscription?.request(.max(arity))
|
||||
|
||||
// Satisfying the unfullfilled demand
|
||||
for (i, publisher) in publishers.reversed().enumerated() {
|
||||
XCTAssertEqual(publisher.send(i), .max(1))
|
||||
}
|
||||
|
||||
XCTAssertEqual(
|
||||
tracking.history,
|
||||
[.subscription("Merge")] + expectedValues + expectedValues.reversed()
|
||||
)
|
||||
|
||||
tracking.cancel()
|
||||
}
|
||||
|
||||
test(forArity: 2) { publishers in
|
||||
Publishers.Merge(publishers[0],
|
||||
publishers[1])
|
||||
}
|
||||
|
||||
test(forArity: 3) { publishers in
|
||||
Publishers.Merge3(publishers[0],
|
||||
publishers[1],
|
||||
publishers[2])
|
||||
}
|
||||
|
||||
test(forArity: 4) { publishers in
|
||||
Publishers.Merge4(publishers[0],
|
||||
publishers[1],
|
||||
publishers[2],
|
||||
publishers[3])
|
||||
}
|
||||
|
||||
test(forArity: 4) { publishers in
|
||||
Publishers.Merge4(publishers[0],
|
||||
publishers[1],
|
||||
publishers[2],
|
||||
publishers[3])
|
||||
}
|
||||
|
||||
test(forArity: 5) { publishers in
|
||||
Publishers.Merge5(publishers[0],
|
||||
publishers[1],
|
||||
publishers[2],
|
||||
publishers[3],
|
||||
publishers[4])
|
||||
}
|
||||
|
||||
test(forArity: 6) { publishers in
|
||||
Publishers.Merge6(publishers[0],
|
||||
publishers[1],
|
||||
publishers[2],
|
||||
publishers[3],
|
||||
publishers[4],
|
||||
publishers[5])
|
||||
}
|
||||
|
||||
test(forArity: 7) { publishers in
|
||||
Publishers.Merge7(publishers[0],
|
||||
publishers[1],
|
||||
publishers[2],
|
||||
publishers[3],
|
||||
publishers[4],
|
||||
publishers[5],
|
||||
publishers[6])
|
||||
}
|
||||
|
||||
test(forArity: 8) { publishers in
|
||||
Publishers.Merge8(publishers[0],
|
||||
publishers[1],
|
||||
publishers[2],
|
||||
publishers[3],
|
||||
publishers[4],
|
||||
publishers[5],
|
||||
publishers[6],
|
||||
publishers[7])
|
||||
}
|
||||
|
||||
test(forArity: 0) { _ in
|
||||
Publishers.MergeMany<CustomPublisher>()
|
||||
}
|
||||
|
||||
test(forArity: 2) { publishers in
|
||||
Publishers.MergeMany(publishers[0], publishers[1])
|
||||
}
|
||||
|
||||
test(forArity: 20) { publishers in
|
||||
Publishers.MergeMany(publishers)
|
||||
}
|
||||
}
|
||||
|
||||
func testMergeUnimitedInitialDemand() {
|
||||
func test<Merger: Publisher>(
|
||||
forArity arity: Int,
|
||||
_ makeMerger: ([CustomPublisher]) -> Merger
|
||||
) where Merger.Output == Int, Merger.Failure == TestingError {
|
||||
let (subscriptions, publishers) = createTestPublishers(arity: arity)
|
||||
let merger = makeMerger(publishers)
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: {
|
||||
$0.request(.unlimited)
|
||||
downstreamSubscription = $0
|
||||
},
|
||||
receiveValue: { _ in .max(3) }
|
||||
)
|
||||
merger.subscribe(tracking)
|
||||
|
||||
for (i, subscription) in subscriptions.enumerated() {
|
||||
XCTAssertEqual(subscription.history,
|
||||
[.requested(.max(1)),
|
||||
.requested(.unlimited)],
|
||||
"failure for subscription \(i)")
|
||||
}
|
||||
|
||||
if arity == 0 {
|
||||
XCTAssertEqual(tracking.history, [.subscription("Merge"),
|
||||
.completion(.finished)])
|
||||
} else {
|
||||
XCTAssertEqual(tracking.history, [.subscription("Merge")])
|
||||
}
|
||||
|
||||
downstreamSubscription?.request(.max(42))
|
||||
downstreamSubscription?.request(.unlimited)
|
||||
|
||||
for (i, subscription) in subscriptions.enumerated() {
|
||||
XCTAssertEqual(subscription.history,
|
||||
[.requested(.max(1)),
|
||||
.requested(.unlimited)],
|
||||
"failure for subscription \(i)")
|
||||
}
|
||||
|
||||
for (i, publisher) in publishers.reversed().enumerated() {
|
||||
XCTAssertEqual(publisher.send(i), .max(3))
|
||||
}
|
||||
|
||||
let expectedValues: [TrackingSubscriber.Event] = (0 ..< arity).map {
|
||||
.value($0)
|
||||
}
|
||||
|
||||
if arity == 0 {
|
||||
XCTAssertEqual(tracking.history, [.subscription("Merge"),
|
||||
.completion(.finished)])
|
||||
} else {
|
||||
XCTAssertEqual(tracking.history,
|
||||
[.subscription("Merge")] + expectedValues)
|
||||
}
|
||||
|
||||
|
||||
for (i, publisher) in publishers.enumerated() {
|
||||
XCTAssertEqual(publisher.send(i), .max(3))
|
||||
}
|
||||
|
||||
if arity == 0 {
|
||||
XCTAssertEqual(tracking.history, [.subscription("Merge"),
|
||||
.completion(.finished)])
|
||||
} else {
|
||||
XCTAssertEqual(tracking.history,
|
||||
[.subscription("Merge")] + expectedValues + expectedValues)
|
||||
}
|
||||
|
||||
tracking.cancel()
|
||||
}
|
||||
|
||||
test(forArity: 2) { publishers in
|
||||
Publishers.Merge(publishers[0],
|
||||
publishers[1])
|
||||
}
|
||||
|
||||
test(forArity: 3) { publishers in
|
||||
Publishers.Merge3(publishers[0],
|
||||
publishers[1],
|
||||
publishers[2])
|
||||
}
|
||||
|
||||
test(forArity: 4) { publishers in
|
||||
Publishers.Merge4(publishers[0],
|
||||
publishers[1],
|
||||
publishers[2],
|
||||
publishers[3])
|
||||
}
|
||||
|
||||
test(forArity: 4) { publishers in
|
||||
Publishers.Merge4(publishers[0],
|
||||
publishers[1],
|
||||
publishers[2],
|
||||
publishers[3])
|
||||
}
|
||||
|
||||
test(forArity: 5) { publishers in
|
||||
Publishers.Merge5(publishers[0],
|
||||
publishers[1],
|
||||
publishers[2],
|
||||
publishers[3],
|
||||
publishers[4])
|
||||
}
|
||||
|
||||
test(forArity: 6) { publishers in
|
||||
Publishers.Merge6(publishers[0],
|
||||
publishers[1],
|
||||
publishers[2],
|
||||
publishers[3],
|
||||
publishers[4],
|
||||
publishers[5])
|
||||
}
|
||||
|
||||
test(forArity: 7) { publishers in
|
||||
Publishers.Merge7(publishers[0],
|
||||
publishers[1],
|
||||
publishers[2],
|
||||
publishers[3],
|
||||
publishers[4],
|
||||
publishers[5],
|
||||
publishers[6])
|
||||
}
|
||||
|
||||
test(forArity: 8) { publishers in
|
||||
Publishers.Merge8(publishers[0],
|
||||
publishers[1],
|
||||
publishers[2],
|
||||
publishers[3],
|
||||
publishers[4],
|
||||
publishers[5],
|
||||
publishers[6],
|
||||
publishers[7])
|
||||
}
|
||||
|
||||
test(forArity: 0) { _ in
|
||||
Publishers.MergeMany<CustomPublisher>()
|
||||
}
|
||||
|
||||
test(forArity: 2) { publishers in
|
||||
Publishers.MergeMany(publishers[0], publishers[1])
|
||||
}
|
||||
|
||||
test(forArity: 20) { publishers in
|
||||
Publishers.MergeMany(publishers)
|
||||
}
|
||||
}
|
||||
|
||||
func testMergeReflection() throws {
|
||||
func testMergeSubscriptionReflection<Sut: Publisher>(_ sut: Sut) throws {
|
||||
try testSubscriptionReflection(
|
||||
description: "Merge",
|
||||
customMirror: childrenIsEmpty,
|
||||
playgroundDescription: "Merge",
|
||||
sut: sut
|
||||
)
|
||||
}
|
||||
func testMergeSideReflection<Merger: Publisher>(
|
||||
_ makeMerger: (CustomPublisher) -> Merger
|
||||
) throws where Merger.Output == Int, Merger.Failure == TestingError {
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: TestingError.self,
|
||||
description: "Merge",
|
||||
customMirror: expectedChildren(
|
||||
("parentSubscription", .anything)
|
||||
),
|
||||
playgroundDescription: "Merge",
|
||||
makeMerger)
|
||||
let publisher = CustomPublisher(subscription: CustomSubscription())
|
||||
let merger = makeMerger(publisher)
|
||||
let tracking = TrackingSubscriber()
|
||||
merger.subscribe(tracking)
|
||||
let side = try XCTUnwrap(publisher.erasedSubscriber)
|
||||
let expectedParentID =
|
||||
try XCTUnwrap(tracking.subscriptions.first?.combineIdentifier)
|
||||
let actualParentID = Mirror(reflecting: side)
|
||||
.descendant("parentSubscription") as? CombineIdentifier
|
||||
XCTAssertEqual(expectedParentID, actualParentID)
|
||||
}
|
||||
|
||||
let publisher = CustomPublisher(subscription: CustomSubscription())
|
||||
|
||||
try testMergeSubscriptionReflection(
|
||||
publisher.merge(with: publisher) as Publishers.Merge
|
||||
)
|
||||
try testMergeSideReflection {
|
||||
$0.merge(with: publisher) as Publishers.Merge
|
||||
}
|
||||
|
||||
try testMergeSubscriptionReflection(
|
||||
publisher.merge(with: publisher,
|
||||
publisher) as Publishers.Merge3
|
||||
)
|
||||
try testMergeSideReflection {
|
||||
$0.merge(with: publisher,
|
||||
publisher) as Publishers.Merge3
|
||||
}
|
||||
|
||||
try testMergeSubscriptionReflection(
|
||||
publisher.merge(with: publisher,
|
||||
publisher,
|
||||
publisher) as Publishers.Merge4
|
||||
)
|
||||
try testMergeSideReflection {
|
||||
$0.merge(with: publisher,
|
||||
publisher,
|
||||
publisher) as Publishers.Merge4
|
||||
}
|
||||
|
||||
try testMergeSubscriptionReflection(
|
||||
publisher.merge(with: publisher,
|
||||
publisher,
|
||||
publisher,
|
||||
publisher) as Publishers.Merge5
|
||||
)
|
||||
try testMergeSideReflection {
|
||||
$0.merge(with: publisher,
|
||||
publisher,
|
||||
publisher,
|
||||
publisher) as Publishers.Merge5
|
||||
}
|
||||
|
||||
try testMergeSubscriptionReflection(
|
||||
publisher.merge(with: publisher,
|
||||
publisher,
|
||||
publisher,
|
||||
publisher,
|
||||
publisher) as Publishers.Merge6
|
||||
)
|
||||
try testMergeSideReflection {
|
||||
$0.merge(with: publisher,
|
||||
publisher,
|
||||
publisher,
|
||||
publisher,
|
||||
publisher) as Publishers.Merge6
|
||||
}
|
||||
|
||||
try testMergeSubscriptionReflection(
|
||||
publisher.merge(with: publisher,
|
||||
publisher,
|
||||
publisher,
|
||||
publisher,
|
||||
publisher,
|
||||
publisher) as Publishers.Merge7
|
||||
)
|
||||
try testMergeSideReflection {
|
||||
$0.merge(with: publisher,
|
||||
publisher,
|
||||
publisher,
|
||||
publisher,
|
||||
publisher,
|
||||
publisher) as Publishers.Merge7
|
||||
}
|
||||
|
||||
try testMergeSubscriptionReflection(
|
||||
publisher.merge(with: publisher,
|
||||
publisher,
|
||||
publisher,
|
||||
publisher,
|
||||
publisher,
|
||||
publisher,
|
||||
publisher) as Publishers.Merge8
|
||||
)
|
||||
try testMergeSideReflection {
|
||||
$0.merge(with: publisher,
|
||||
publisher,
|
||||
publisher,
|
||||
publisher,
|
||||
publisher,
|
||||
publisher,
|
||||
publisher) as Publishers.Merge8
|
||||
}
|
||||
|
||||
try testMergeSubscriptionReflection(
|
||||
publisher.merge(with: publisher) as Publishers.MergeMany
|
||||
)
|
||||
try testMergeSideReflection {
|
||||
$0.merge(with: $0) as Publishers.MergeMany
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,353 @@
|
||||
//
|
||||
// ReceiveOnTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 02.12.2019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class ReceiveOnTests: XCTestCase {
|
||||
|
||||
func testBasicBehavior() {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(100),
|
||||
receiveValueDemand: .max(12)) {
|
||||
$0.receive(on: scheduler, options: .nontrivialOptions)
|
||||
}
|
||||
XCTAssertNotNil(helper.publisher.subscriber,
|
||||
"Subscription must be performed synchronously")
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("ReceiveOn")])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(100))])
|
||||
XCTAssertEqual(scheduler.history, [])
|
||||
|
||||
scheduler.executeScheduledActions()
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("ReceiveOn")])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(100))])
|
||||
XCTAssertEqual(scheduler.history, [])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(1), .none)
|
||||
XCTAssertEqual(helper.publisher.send(2), .none)
|
||||
XCTAssertEqual(helper.publisher.send(3), .none)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("ReceiveOn")])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(100))])
|
||||
XCTAssertEqual(scheduler.scheduledDates, [.nanoseconds(0),
|
||||
.nanoseconds(0),
|
||||
.nanoseconds(0)])
|
||||
|
||||
XCTAssertEqual(scheduler.history, [.schedule(options: .nontrivialOptions),
|
||||
.schedule(options: .nontrivialOptions),
|
||||
.schedule(options: .nontrivialOptions)])
|
||||
|
||||
scheduler.executeScheduledActions()
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("ReceiveOn"),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3)])
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(100)),
|
||||
.requested(.max(12)),
|
||||
.requested(.max(12)),
|
||||
.requested(.max(12))])
|
||||
|
||||
XCTAssertEqual(scheduler.history, [.schedule(options: .nontrivialOptions),
|
||||
.schedule(options: .nontrivialOptions),
|
||||
.schedule(options: .nontrivialOptions)])
|
||||
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
helper.publisher.send(completion: .finished)
|
||||
XCTAssertEqual(helper.publisher.send(4), .none)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("ReceiveOn"),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(100)),
|
||||
.requested(.max(12)),
|
||||
.requested(.max(12)),
|
||||
.requested(.max(12))])
|
||||
XCTAssertEqual(scheduler.scheduledDates, [.nanoseconds(0)])
|
||||
XCTAssertEqual(scheduler.history, [.schedule(options: .nontrivialOptions),
|
||||
.schedule(options: .nontrivialOptions),
|
||||
.schedule(options: .nontrivialOptions),
|
||||
.schedule(options: .nontrivialOptions)])
|
||||
scheduler.executeScheduledActions()
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("ReceiveOn"),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3),
|
||||
.completion(.failure(.oops))])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(100)),
|
||||
.requested(.max(12)),
|
||||
.requested(.max(12)),
|
||||
.requested(.max(12))])
|
||||
XCTAssertEqual(scheduler.history, [.schedule(options: .nontrivialOptions),
|
||||
.schedule(options: .nontrivialOptions),
|
||||
.schedule(options: .nontrivialOptions),
|
||||
.schedule(options: .nontrivialOptions)])
|
||||
XCTAssertEqual(scheduler.now, .nanoseconds(0))
|
||||
}
|
||||
|
||||
func testRequest() throws {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .none) {
|
||||
$0.receive(on: scheduler)
|
||||
}
|
||||
scheduler.executeScheduledActions()
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("ReceiveOn")])
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(10))
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(4))
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(5))
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.none)
|
||||
XCTAssertEqual(helper.publisher.send(2000), .none)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("ReceiveOn")])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(10)),
|
||||
.requested(.max(4)),
|
||||
.requested(.max(5)),
|
||||
.requested(.none)])
|
||||
|
||||
scheduler.executeScheduledActions()
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("ReceiveOn"),
|
||||
.value(2000)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(10)),
|
||||
.requested(.max(4)),
|
||||
.requested(.max(5)),
|
||||
.requested(.none)])
|
||||
}
|
||||
|
||||
func testCancelAlreadyCancelled() throws {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .none) {
|
||||
$0.receive(on: scheduler)
|
||||
}
|
||||
|
||||
scheduler.executeScheduledActions()
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("ReceiveOn")])
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(42))
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited), .cancelled])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("ReceiveOn")])
|
||||
XCTAssertEqual(scheduler.history, [])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(0), .none)
|
||||
helper.publisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited), .cancelled])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("ReceiveOn")])
|
||||
XCTAssertEqual(scheduler.history, [])
|
||||
}
|
||||
|
||||
func testReceiveCompletionImmediatelyAfterSubscription() {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .none) {
|
||||
$0.receive(on: scheduler)
|
||||
}
|
||||
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("ReceiveOn")])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(scheduler.history, [.schedule(options: nil)])
|
||||
|
||||
scheduler.executeScheduledActions()
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("ReceiveOn"),
|
||||
.completion(.failure(.oops))])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
}
|
||||
|
||||
func testReceiveCompletionImmediatelyAfterValue() {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .max(418)) {
|
||||
$0.receive(on: scheduler)
|
||||
}
|
||||
XCTAssertEqual(helper.publisher.send(-1), .none)
|
||||
scheduler.executeScheduledActions()
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(1000), .none)
|
||||
helper.publisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("ReceiveOn"),
|
||||
.value(-1)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
|
||||
.requested(.max(418))])
|
||||
XCTAssertEqual(scheduler.history, [.schedule(options: nil),
|
||||
.schedule(options: nil),
|
||||
.schedule(options: nil)])
|
||||
|
||||
scheduler.executeScheduledActions()
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("ReceiveOn"),
|
||||
.value(-1),
|
||||
.value(1000),
|
||||
.completion(.finished)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
|
||||
.requested(.max(418))])
|
||||
}
|
||||
|
||||
func testReceiveInputRecursively() {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .max(418)) {
|
||||
$0.receive(on: ImmediateScheduler.shared)
|
||||
}
|
||||
|
||||
var recursionCounter = 5
|
||||
helper.tracking.onValue = { _ in
|
||||
if recursionCounter == 0 { return }
|
||||
recursionCounter -= 1
|
||||
_ = helper.publisher.send(-1)
|
||||
}
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(0), .none)
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("ReceiveOn"),
|
||||
.value(0),
|
||||
.value(-1),
|
||||
.value(-1),
|
||||
.value(-1),
|
||||
.value(-1),
|
||||
.value(-1)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
|
||||
.requested(.max(418)),
|
||||
.requested(.max(418)),
|
||||
.requested(.max(418)),
|
||||
.requested(.max(418)),
|
||||
.requested(.max(418)),
|
||||
.requested(.max(418))])
|
||||
}
|
||||
|
||||
func testReceiveCompletionRecursively() {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .max(418)) {
|
||||
$0.receive(on: ImmediateScheduler.shared)
|
||||
}
|
||||
helper.tracking.onFinish = {
|
||||
helper.publisher.send(completion: .finished)
|
||||
}
|
||||
helper.publisher.send(completion: .finished)
|
||||
}
|
||||
|
||||
func testWeakCaptureWhenSchedulingValue() {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
var value: Int?
|
||||
var subscriberReleased = false
|
||||
do {
|
||||
let publisher = CustomPublisher(subscription: CustomSubscription())
|
||||
let receiveOn = publisher.receive(on: scheduler)
|
||||
let tracking = TrackingSubscriber(receiveValue: { value = $0; return .none },
|
||||
onDeinit: { subscriberReleased = true })
|
||||
receiveOn.subscribe(tracking)
|
||||
scheduler.executeScheduledActions()
|
||||
XCTAssertEqual(tracking.history, [.subscription("ReceiveOn")])
|
||||
XCTAssertEqual(publisher.send(42), .none)
|
||||
XCTAssertEqual(tracking.history, [.subscription("ReceiveOn")])
|
||||
XCTAssertEqual(scheduler.history, [.schedule(options: nil)])
|
||||
tracking.cancel()
|
||||
publisher.cancel()
|
||||
}
|
||||
XCTAssertFalse(subscriberReleased)
|
||||
scheduler.executeScheduledActions()
|
||||
XCTAssertNil(value)
|
||||
XCTAssertTrue(subscriberReleased)
|
||||
}
|
||||
|
||||
func testWeakCaptureWhenSchedulingCompletion() {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
var completion: Subscribers.Completion<TestingError>?
|
||||
var subscriberReleased = false
|
||||
do {
|
||||
let publisher = CustomPublisher(subscription: CustomSubscription())
|
||||
let receiveOn = publisher.receive(on: scheduler)
|
||||
let tracking = TrackingSubscriber(receiveCompletion: { completion = $0 },
|
||||
onDeinit: { subscriberReleased = true })
|
||||
receiveOn.subscribe(tracking)
|
||||
scheduler.executeScheduledActions()
|
||||
XCTAssertEqual(tracking.history, [.subscription("ReceiveOn")])
|
||||
publisher.send(completion: .finished)
|
||||
XCTAssertEqual(tracking.history, [.subscription("ReceiveOn")])
|
||||
XCTAssertEqual(scheduler.history, [.schedule(options: nil)])
|
||||
tracking.cancel()
|
||||
publisher.cancel()
|
||||
}
|
||||
XCTAssertFalse(subscriberReleased)
|
||||
scheduler.executeScheduledActions()
|
||||
XCTAssertNil(completion)
|
||||
XCTAssertTrue(subscriberReleased)
|
||||
}
|
||||
|
||||
func testReceiveOnReceiveSubscriptionTwice() throws {
|
||||
try testReceiveSubscriptionTwice {
|
||||
$0.receive(on: ImmediateScheduler.shared)
|
||||
}
|
||||
}
|
||||
|
||||
func testReceiveOnReceiveValueBeforeSubscription() {
|
||||
testReceiveValueBeforeSubscription(value: 213,
|
||||
expected: .history([], demand: .none)) {
|
||||
$0.receive(on: ImmediateScheduler.shared)
|
||||
}
|
||||
}
|
||||
|
||||
func testReceiveOnReceiveCompletionBeforeSubscription() {
|
||||
testReceiveCompletionBeforeSubscription(inputType: Int.self,
|
||||
expected: .history([])) {
|
||||
$0.receive(on: ImmediateScheduler.shared)
|
||||
}
|
||||
}
|
||||
|
||||
func testReceiveOnRequestBeforeSubscription() {
|
||||
testRequestBeforeSubscription(inputType: Int.self, shouldCrash: false) {
|
||||
$0.receive(on: ImmediateScheduler.shared)
|
||||
}
|
||||
}
|
||||
|
||||
func testReceiveOnCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self, shouldCrash: false) {
|
||||
$0.receive(on: ImmediateScheduler.shared)
|
||||
}
|
||||
}
|
||||
|
||||
func testReceiveOnReflection() throws {
|
||||
try testReflection(parentInput: String.self,
|
||||
parentFailure: Error.self,
|
||||
description: "ReceiveOn",
|
||||
customMirror: childrenIsEmpty,
|
||||
playgroundDescription: "ReceiveOn",
|
||||
{ $0.receive(on: ImmediateScheduler.shared) })
|
||||
}
|
||||
|
||||
func testReceiveOnLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31, cancellingSubscriptionReleasesSubscriber: true) {
|
||||
$0.receive(on: ImmediateScheduler.shared)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
//
|
||||
// ReplaceEmptyTests.swift
|
||||
// OpenCombine
|
||||
//
|
||||
// Created by Joseph Spadafora on 12/10/19.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class ReplaceEmptyTests: XCTestCase {
|
||||
func testEmptySubscription() {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .none,
|
||||
createSut: { $0.replaceEmpty(with: 15) }
|
||||
)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("ReplaceEmpty")])
|
||||
}
|
||||
|
||||
func testError() {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(1),
|
||||
receiveValueDemand: .none,
|
||||
createSut: { $0.replaceEmpty(with: 42) }
|
||||
)
|
||||
|
||||
helper.publisher.send(completion: .failure(TestingError.oops))
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("ReplaceEmpty"),
|
||||
.completion(.failure(.oops))])
|
||||
}
|
||||
|
||||
func testEndWithoutValueReplacesCorrectly() {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(1),
|
||||
receiveValueDemand: .none,
|
||||
createSut: { $0.replaceEmpty(with: 42) }
|
||||
)
|
||||
|
||||
helper.publisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("ReplaceEmpty"),
|
||||
.value(42),
|
||||
.completion(.finished)])
|
||||
}
|
||||
|
||||
func testNoValueIsReplacedIfEndsWithoutEmpty() {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(1),
|
||||
receiveValueDemand: .none,
|
||||
createSut: { $0.replaceEmpty(with: 42) }
|
||||
)
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(3), .none)
|
||||
helper.publisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("ReplaceEmpty"),
|
||||
.value(3),
|
||||
.completion(.finished)])
|
||||
}
|
||||
|
||||
func testSendingValueAndThenError() {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(1),
|
||||
receiveValueDemand: .max(1),
|
||||
createSut: { $0.replaceEmpty(with: 42) })
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(8), .max(1))
|
||||
helper.publisher.send(completion: .failure(TestingError.oops))
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("ReplaceEmpty"),
|
||||
.value(8),
|
||||
.completion(.failure(.oops))])
|
||||
}
|
||||
|
||||
func testFailingBeforeDemanding() {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .max(1),
|
||||
createSut: { $0.replaceEmpty(with: 42) })
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("ReplaceEmpty")])
|
||||
|
||||
helper.publisher.send(completion: .failure(TestingError.oops))
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("ReplaceEmpty"),
|
||||
.completion(.failure(.oops))])
|
||||
|
||||
helper.downstreamSubscription?.request(.unlimited)
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("ReplaceEmpty"),
|
||||
.completion(.failure(.oops))])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(-1), .none)
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("ReplaceEmpty"),
|
||||
.completion(.failure(.oops))])
|
||||
}
|
||||
|
||||
func testUpstreamCompletesEmptyBeforeDownstreamRequests() {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .none,
|
||||
createSut: { $0.replaceEmpty(with: 22) })
|
||||
helper.publisher.send(completion: .finished)
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("ReplaceEmpty")])
|
||||
|
||||
helper.subscription.request(.max(3))
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("ReplaceEmpty")])
|
||||
|
||||
helper.downstreamSubscription?.request(.max(1))
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("ReplaceEmpty"),
|
||||
.value(22),
|
||||
.completion(.finished)])
|
||||
}
|
||||
|
||||
// MARK: - Basic Behavior
|
||||
func testBasicBehavior() {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .none,
|
||||
createSut: { $0.replaceEmpty(with: 22) })
|
||||
XCTAssertEqual(helper.publisher.send(2), .none)
|
||||
XCTAssertEqual(helper.publisher.send(4), .none)
|
||||
XCTAssertEqual(helper.publisher.send(6), .none)
|
||||
XCTAssertEqual(helper.publisher.send(7), .none)
|
||||
XCTAssertEqual(helper.publisher.send(8), .none)
|
||||
XCTAssertEqual(helper.publisher.send(9), .none)
|
||||
helper.publisher.send(completion: .finished)
|
||||
XCTAssertEqual(helper.publisher.send(10), .none)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("ReplaceEmpty"),
|
||||
.value(2),
|
||||
.value(4),
|
||||
.value(6),
|
||||
.value(7),
|
||||
.value(8),
|
||||
.value(9),
|
||||
.completion(.finished)])
|
||||
}
|
||||
|
||||
func testDemand() throws {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(42),
|
||||
receiveValueDemand: .max(4),
|
||||
createSut: { $0.replaceEmpty(with: 832) })
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(42)),
|
||||
.requested(.unlimited)])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(0), .max(4))
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(42)),
|
||||
.requested(.unlimited)])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(2), .max(4))
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(42)),
|
||||
.requested(.unlimited)])
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(95))
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(5))
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(42)),
|
||||
.requested(.unlimited),
|
||||
.requested(.max(95)),
|
||||
.requested(.max(5))])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(3), .max(4))
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(42)),
|
||||
.requested(.unlimited),
|
||||
.requested(.max(95)),
|
||||
.requested(.max(5))])
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(121))
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(42)),
|
||||
.requested(.unlimited),
|
||||
.requested(.max(95)),
|
||||
.requested(.max(5)),
|
||||
.requested(.max(121))])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(7), .max(4))
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(42)),
|
||||
.requested(.unlimited),
|
||||
.requested(.max(95)),
|
||||
.requested(.max(5)),
|
||||
.requested(.max(121))])
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(42)),
|
||||
.requested(.unlimited),
|
||||
.requested(.max(95)),
|
||||
.requested(.max(5)),
|
||||
.requested(.max(121)),
|
||||
.cancelled])
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(50))
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(42)),
|
||||
.requested(.unlimited),
|
||||
.requested(.max(95)),
|
||||
.requested(.max(5)),
|
||||
.requested(.max(121)),
|
||||
.cancelled])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(8), .none)
|
||||
}
|
||||
|
||||
func testImmediateCompletion() {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .none,
|
||||
createSut: { $0.replaceEmpty(with: 33) })
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
|
||||
.requested(.unlimited)])
|
||||
helper.publisher.send(completion: .finished)
|
||||
helper.publisher.send(completion: .finished)
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
|
||||
.requested(.unlimited)])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("ReplaceEmpty"),
|
||||
.value(33),
|
||||
.completion(.finished)])
|
||||
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("ReplaceEmpty"),
|
||||
.value(33),
|
||||
.completion(.finished)])
|
||||
}
|
||||
|
||||
func testCancelAlreadyCancelled() throws {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .none,
|
||||
createSut: { $0.replaceEmpty(with: -7) })
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.unlimited)
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
|
||||
.requested(.unlimited),
|
||||
.cancelled])
|
||||
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
helper.publisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
|
||||
.requested(.unlimited),
|
||||
.cancelled])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("ReplaceEmpty")])
|
||||
}
|
||||
|
||||
func testReceiveSubscriptionTwice() throws {
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .none,
|
||||
createSut: { $0.replaceEmpty(with: 22) }
|
||||
)
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
|
||||
let secondSubscription = CustomSubscription()
|
||||
|
||||
try XCTUnwrap(helper.publisher.subscriber)
|
||||
.receive(subscription: secondSubscription)
|
||||
|
||||
XCTAssertEqual(secondSubscription.history, [.cancelled])
|
||||
|
||||
try XCTUnwrap(helper.publisher.subscriber)
|
||||
.receive(subscription: helper.subscription)
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited), .cancelled])
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
|
||||
.cancelled,
|
||||
.cancelled])
|
||||
}
|
||||
|
||||
func testReplaceEmptyReflection() throws {
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: Error.self,
|
||||
description: "ReplaceEmpty",
|
||||
customMirror: childrenIsEmpty,
|
||||
playgroundDescription: "ReplaceEmpty",
|
||||
{ $0.replaceEmpty(with: 0) })
|
||||
}
|
||||
|
||||
func testCrashesWhenRequestedZeroDemand() {
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .none,
|
||||
createSut: { $0.replaceEmpty(with: 9) }
|
||||
)
|
||||
|
||||
assertCrashes {
|
||||
helper.downstreamSubscription?.request(.none)
|
||||
}
|
||||
}
|
||||
|
||||
func testReplaceEmptyReceiveValueBeforeSubscription() {
|
||||
testReceiveValueBeforeSubscription(value: 213,
|
||||
expected: .history([], demand: .none)) {
|
||||
$0.replaceEmpty(with: 742)
|
||||
}
|
||||
}
|
||||
|
||||
func testReplaceEmptyReceiveCompletionBeforeSubscription() {
|
||||
testReceiveCompletionBeforeSubscription(inputType: Int.self,
|
||||
expected: .history([])) {
|
||||
$0.replaceEmpty(with: -14)
|
||||
}
|
||||
}
|
||||
|
||||
func testReplaceEmptyRequestBeforeSubscription() {
|
||||
testRequestBeforeSubscription(inputType: Int.self, shouldCrash: false) {
|
||||
$0.replaceEmpty(with: 19)
|
||||
}
|
||||
}
|
||||
|
||||
func testReplaceEmptyCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self, shouldCrash: false) {
|
||||
$0.replaceEmpty(with: 1337)
|
||||
}
|
||||
}
|
||||
|
||||
func testReplaceEmptyLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
finishingIsPassedThrough: false,
|
||||
{ $0.replaceEmpty(with: 13) })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,333 @@
|
||||
//
|
||||
// SubscribeOnTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 03.12.2019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class SubscribeOnTests: XCTestCase {
|
||||
|
||||
func testSynchronouslySendsEventsDownstream() throws {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let subscribeOn = publisher.subscribe(on: scheduler, options: .nontrivialOptions)
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: { $0.request(.max(100)) },
|
||||
receiveValue: { _ in .max(12) }
|
||||
)
|
||||
|
||||
publisher.didSubscribe = { _ in
|
||||
XCTAssertEqual(tracking.history,
|
||||
[.subscription("SubscribeOn")],
|
||||
"Subscription object should be sent synchronously")
|
||||
XCTAssertEqual(subscription.history,
|
||||
[],
|
||||
"Demand should be requested asynchronously")
|
||||
}
|
||||
|
||||
subscribeOn.subscribe(tracking)
|
||||
|
||||
XCTAssertNil(publisher.subscriber,
|
||||
"Subscription must be performed asynchronously")
|
||||
|
||||
XCTAssertEqual(tracking.history, [])
|
||||
XCTAssertEqual(subscription.history, [])
|
||||
XCTAssertEqual(scheduler.history, [.schedule(options: .nontrivialOptions)])
|
||||
|
||||
scheduler.executeScheduledActions()
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("SubscribeOn")])
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(100))])
|
||||
|
||||
XCTAssertEqual(publisher.send(1), .max(12))
|
||||
XCTAssertEqual(publisher.send(2), .max(12))
|
||||
XCTAssertEqual(publisher.send(3), .max(12))
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("SubscribeOn"),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3)])
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(100))])
|
||||
XCTAssertEqual(scheduler.history, [.schedule(options: .nontrivialOptions),
|
||||
.schedule(options: .nontrivialOptions)])
|
||||
|
||||
publisher.send(completion: .finished)
|
||||
publisher.send(completion: .failure(.oops))
|
||||
XCTAssertEqual(publisher.send(-1), .none)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("SubscribeOn"),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3),
|
||||
.completion(.finished)])
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(100))])
|
||||
XCTAssertEqual(scheduler.history, [.schedule(options: .nontrivialOptions),
|
||||
.schedule(options: .nontrivialOptions)])
|
||||
}
|
||||
|
||||
func testAsynchronouslySendsEventsUpstream() throws {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .unlimited) {
|
||||
$0.subscribe(on: scheduler, options: .nontrivialOptions)
|
||||
}
|
||||
scheduler.executeScheduledActions()
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("SubscribeOn")])
|
||||
XCTAssertEqual(helper.subscription.history, [])
|
||||
XCTAssertEqual(scheduler.history, [.schedule(options: .nontrivialOptions)])
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(17))
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.unlimited)
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.none)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("SubscribeOn")])
|
||||
XCTAssertEqual(helper.subscription.history, [])
|
||||
XCTAssertEqual(scheduler.history, [.schedule(options: .nontrivialOptions),
|
||||
.schedule(options: .nontrivialOptions),
|
||||
.schedule(options: .nontrivialOptions),
|
||||
.schedule(options: .nontrivialOptions)])
|
||||
|
||||
scheduler.executeScheduledActions()
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("SubscribeOn")])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(17)),
|
||||
.requested(.unlimited),
|
||||
.requested(.none)])
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("SubscribeOn")])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(17)),
|
||||
.requested(.unlimited),
|
||||
.requested(.none)])
|
||||
XCTAssertEqual(scheduler.history, [.schedule(options: .nontrivialOptions),
|
||||
.schedule(options: .nontrivialOptions),
|
||||
.schedule(options: .nontrivialOptions),
|
||||
.schedule(options: .nontrivialOptions),
|
||||
.schedule(options: .nontrivialOptions)])
|
||||
|
||||
scheduler.executeScheduledActions()
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("SubscribeOn")])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(17)),
|
||||
.requested(.unlimited),
|
||||
.requested(.none),
|
||||
.cancelled])
|
||||
}
|
||||
|
||||
func testCancelAlreadyCancelled() throws {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .max(42)) {
|
||||
$0.subscribe(on: scheduler)
|
||||
}
|
||||
scheduler.executeScheduledActions()
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("SubscribeOn")])
|
||||
XCTAssertEqual(helper.subscription.history, [])
|
||||
XCTAssertEqual(scheduler.history, [.schedule(options: nil)])
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
XCTAssertEqual(helper.publisher.send(1000), .none)
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("SubscribeOn")])
|
||||
XCTAssertEqual(helper.subscription.history, [])
|
||||
XCTAssertEqual(scheduler.history, [.schedule(options: nil),
|
||||
.schedule(options: nil)])
|
||||
|
||||
scheduler.executeScheduledActions()
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("SubscribeOn")])
|
||||
XCTAssertEqual(helper.subscription.history, [.cancelled])
|
||||
XCTAssertEqual(scheduler.history, [.schedule(options: nil),
|
||||
.schedule(options: nil)])
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(20000))
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
|
||||
XCTAssertEqual(scheduler.history, [.schedule(options: nil),
|
||||
.schedule(options: nil)])
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("SubscribeOn")])
|
||||
XCTAssertEqual(helper.subscription.history, [.cancelled])
|
||||
XCTAssertEqual(scheduler.history, [.schedule(options: nil),
|
||||
.schedule(options: nil)])
|
||||
}
|
||||
|
||||
func testCancelImmediatelyAfterRequest() throws {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .max(42)) {
|
||||
$0.subscribe(on: scheduler)
|
||||
}
|
||||
scheduler.executeScheduledActions()
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("SubscribeOn")])
|
||||
XCTAssertEqual(helper.subscription.history, [])
|
||||
XCTAssertEqual(scheduler.history, [.schedule(options: nil)])
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.unlimited)
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("SubscribeOn")])
|
||||
XCTAssertEqual(helper.subscription.history, [])
|
||||
XCTAssertEqual(scheduler.history, [.schedule(options: nil),
|
||||
.schedule(options: nil),
|
||||
.schedule(options: nil)])
|
||||
|
||||
scheduler.executeScheduledActions()
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("SubscribeOn")])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited), .cancelled])
|
||||
XCTAssertEqual(scheduler.history, [.schedule(options: nil),
|
||||
.schedule(options: nil),
|
||||
.schedule(options: nil)])
|
||||
}
|
||||
|
||||
func testCrashWhenRequestingRecursively() throws {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .none) {
|
||||
$0.subscribe(on: ImmediateScheduler.shared)
|
||||
}
|
||||
|
||||
var recursionCounter = 5
|
||||
helper.subscription.onRequest = { _ in
|
||||
if recursionCounter == 0 { return }
|
||||
recursionCounter -= 1
|
||||
helper.downstreamSubscription?.request(.unlimited)
|
||||
}
|
||||
|
||||
try assertCrashes {
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(1))
|
||||
}
|
||||
}
|
||||
|
||||
func testCancelRecursively() throws {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .none) {
|
||||
$0.subscribe(on: ImmediateScheduler.shared)
|
||||
}
|
||||
|
||||
helper.subscription.onCancel = {
|
||||
helper.downstreamSubscription?.cancel()
|
||||
}
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
}
|
||||
|
||||
func testWeakCaptureWhenSchedulingRequest() throws {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
var subscriberReleased = false
|
||||
let subscription = CustomSubscription()
|
||||
do {
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let subscribeOn = publisher.subscribe(on: scheduler)
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: { downstreamSubscription = $0 },
|
||||
onDeinit: { subscriberReleased = true }
|
||||
)
|
||||
subscribeOn.subscribe(tracking)
|
||||
scheduler.executeScheduledActions()
|
||||
try XCTUnwrap(downstreamSubscription).request(.max(1))
|
||||
XCTAssertEqual(subscription.history, [])
|
||||
XCTAssertEqual(scheduler.history, [.schedule(options: nil),
|
||||
.schedule(options: nil)])
|
||||
publisher.cancel()
|
||||
tracking.cancel()
|
||||
}
|
||||
XCTAssertTrue(subscriberReleased)
|
||||
scheduler.executeScheduledActions()
|
||||
XCTAssertEqual(subscription.history, [])
|
||||
}
|
||||
|
||||
func testWeakCaptureWhenSchedulingCancel() throws {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
var subscriberReleased = false
|
||||
let subscription = CustomSubscription()
|
||||
do {
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let subscribeOn = publisher.subscribe(on: scheduler)
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: { downstreamSubscription = $0 },
|
||||
onDeinit: { subscriberReleased = true }
|
||||
)
|
||||
subscribeOn.subscribe(tracking)
|
||||
scheduler.executeScheduledActions()
|
||||
try XCTUnwrap(downstreamSubscription).cancel()
|
||||
XCTAssertEqual(subscription.history, [])
|
||||
XCTAssertEqual(scheduler.history, [.schedule(options: nil),
|
||||
.schedule(options: nil)])
|
||||
publisher.cancel()
|
||||
tracking.cancel()
|
||||
}
|
||||
XCTAssertTrue(subscriberReleased)
|
||||
scheduler.executeScheduledActions()
|
||||
XCTAssertEqual(subscription.history, [])
|
||||
}
|
||||
|
||||
func testSubscribeOnReceiveSubscriptionTwice() throws {
|
||||
try testReceiveSubscriptionTwice {
|
||||
$0.subscribe(on: ImmediateScheduler.shared)
|
||||
}
|
||||
}
|
||||
|
||||
func testSubscribeOnReceiveValueBeforeSubscription() {
|
||||
testReceiveValueBeforeSubscription(value: 213,
|
||||
expected: .history([], demand: .none)) {
|
||||
$0.subscribe(on: ImmediateScheduler.shared)
|
||||
}
|
||||
}
|
||||
|
||||
func testSubscribeOnReceiveCompletionBeforeSubscription() {
|
||||
testReceiveCompletionBeforeSubscription(inputType: Int.self,
|
||||
expected: .history([])) {
|
||||
$0.subscribe(on: ImmediateScheduler.shared)
|
||||
}
|
||||
}
|
||||
|
||||
func testSubscribeOnRequestBeforeSubscription() {
|
||||
testRequestBeforeSubscription(inputType: Int.self, shouldCrash: false) {
|
||||
$0.subscribe(on: ImmediateScheduler.shared)
|
||||
}
|
||||
}
|
||||
|
||||
func testSubscribeOnCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self, shouldCrash: false) {
|
||||
$0.subscribe(on: ImmediateScheduler.shared)
|
||||
}
|
||||
}
|
||||
|
||||
func testSubscribeOnReflection() throws {
|
||||
try testReflection(parentInput: Double.self,
|
||||
parentFailure: Error.self,
|
||||
description: "SubscribeOn",
|
||||
customMirror: childrenIsEmpty,
|
||||
playgroundDescription: "SubscribeOn",
|
||||
{ $0.subscribe(on: ImmediateScheduler.shared) })
|
||||
}
|
||||
|
||||
func testSubscribeOnLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31, cancellingSubscriptionReleasesSubscriber: true) {
|
||||
$0.subscribe(on: ImmediateScheduler.shared)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
//
|
||||
// VirtualTimeSchedulerTests.swift
|
||||
// OpenCombineTests
|
||||
//
|
||||
// Created by Евгений Богомолов on 14/09/2019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class VirtualTimeSchedulerTests: XCTestCase {
|
||||
|
||||
func testOrder() {
|
||||
var history = [Int]()
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
scheduler.schedule(after: scheduler.now + .nanoseconds(10)) {
|
||||
history.append(5)
|
||||
}
|
||||
scheduler.schedule {
|
||||
history.append(1)
|
||||
}
|
||||
scheduler.schedule(after: scheduler.now + .nanoseconds(5)) {
|
||||
history.append(3)
|
||||
scheduler.schedule(after: scheduler.now + .nanoseconds(2)) {
|
||||
history.append(4)
|
||||
}
|
||||
}
|
||||
scheduler.schedule {
|
||||
history.append(2)
|
||||
}
|
||||
|
||||
XCTAssertEqual(scheduler.now, .nanoseconds(0))
|
||||
XCTAssertEqual(scheduler.scheduledDates, [.nanoseconds(0),
|
||||
.nanoseconds(0),
|
||||
.nanoseconds(5),
|
||||
.nanoseconds(10)])
|
||||
|
||||
scheduler.executeScheduledActions()
|
||||
XCTAssertEqual(history, [1, 2, 3, 4, 5])
|
||||
XCTAssertEqual(scheduler.now, .nanoseconds(10))
|
||||
XCTAssertEqual(scheduler.scheduledDates, [])
|
||||
|
||||
XCTAssertEqual(scheduler.history, [.now,
|
||||
.minimumTolerance,
|
||||
.scheduleAfterDate(.nanoseconds(10),
|
||||
tolerance: 0,
|
||||
options: nil),
|
||||
.schedule(options: nil),
|
||||
.now,
|
||||
.minimumTolerance,
|
||||
.scheduleAfterDate(.nanoseconds(5),
|
||||
tolerance: 0,
|
||||
options: nil),
|
||||
.schedule(options: nil),
|
||||
.now,
|
||||
.now,
|
||||
.minimumTolerance,
|
||||
.scheduleAfterDate(.nanoseconds(7),
|
||||
tolerance: 0,
|
||||
options: nil),
|
||||
.now])
|
||||
}
|
||||
|
||||
func testRepeadedAction() {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
var history = [Int]()
|
||||
let cancellable = scheduler.schedule(after: scheduler.now + .microseconds(2),
|
||||
interval: .milliseconds(40)) {
|
||||
history.append(Int(scheduler.now.time))
|
||||
}
|
||||
scheduler.schedule(after: scheduler.now + .milliseconds(300)) {
|
||||
cancellable.cancel()
|
||||
}
|
||||
XCTAssertEqual(scheduler.scheduledDates, [.microseconds(2), .milliseconds(300)])
|
||||
scheduler.executeScheduledActions()
|
||||
|
||||
XCTAssertEqual(history, [2000,
|
||||
40002000,
|
||||
80002000,
|
||||
120002000,
|
||||
160002000,
|
||||
200002000,
|
||||
240002000,
|
||||
280002000])
|
||||
XCTAssertEqual(scheduler.now, .microseconds(320002))
|
||||
XCTAssertEqual(scheduler.history,
|
||||
[.now,
|
||||
.minimumTolerance,
|
||||
.scheduleAfterDateWithInterval(.microseconds(2),
|
||||
interval: .milliseconds(40),
|
||||
tolerance: 0,
|
||||
options: nil),
|
||||
.now,
|
||||
.minimumTolerance,
|
||||
.scheduleAfterDate(.milliseconds(300),
|
||||
tolerance: .nanoseconds(0),
|
||||
options: nil),
|
||||
.now,
|
||||
.now,
|
||||
.now,
|
||||
.now,
|
||||
.now,
|
||||
.now,
|
||||
.now,
|
||||
.now,
|
||||
.now])
|
||||
}
|
||||
}
|
||||
@@ -3,3 +3,7 @@ def suffix_variadic(name, index, arity):
|
||||
|
||||
def list_with_suffix_variadic(name, arity):
|
||||
return [suffix_variadic(name, i, arity) for i in range(arity)]
|
||||
|
||||
def indent(input, space_count):
|
||||
padding = space_count * ' '
|
||||
return ''.join(padding + line for line in input.splitlines(True))
|
||||
|
||||
Reference in New Issue
Block a user