25 Commits

Author SHA1 Message Date
Sergej Jaskiewicz af0ae86407 WIP Publishers.Merge 2020-01-06 21:36:34 +03:00
Sergej Jaskiewicz f861335dc3 Implement Publishers.AssertNoFailure (#138) 2019-12-25 19:15:57 +03:00
Sergej Jaskiewicz 769c3c818f Implement Publishers.CollectByCount (#137) 2019-12-25 03:01:34 +03:00
Sergej Jaskiewicz 910d21da4c Implement Publishers.DropUntilOutput (#136) 2019-12-24 22:14:10 +03:00
Sergej Jaskiewicz 6e20956d6d Guess unknown DisaptchTimeInterval more precisely (#135) 2019-12-24 19:18:29 +03:00
dependabot[bot] e453879d75 Bump excon from 0.68.0 to 0.71.0 (#132)
Bumps [excon](https://github.com/excon/excon) from 0.68.0 to 0.71.0.
- [Release notes](https://github.com/excon/excon/releases)
- [Changelog](https://github.com/excon/excon/blob/master/changelog.txt)
- [Commits](https://github.com/excon/excon/compare/v0.68.0...v0.71.0)

Signed-off-by: dependabot[bot] <support@github.com>
2019-12-17 02:25:13 +03:00
Sergej Jaskiewicz 98f6b6b337 Fix more overflows in DispatchQueue.SchedulerTimeType (#130) 2019-12-15 15:57:05 +03:00
Sergej Jaskiewicz 74b739d74e Work around the 'default will never be executed' warning on Linux (#129)
Also, enable the -warnings-as-errors flag on CI.
2019-12-15 02:37:33 +03:00
Sergej Jaskiewicz bcba9a19d4 Update for Xcode 11.3 (#123)
- Send subscription synchronously in ReceiveOn and Delay operators
- Some locks made recursive, as they should be
- ObservableObjectPublisher doesn't use PassthroughSubject under the hood anymore
2019-12-14 23:11:47 +03:00
Max Desiatov 486e166462 Expose OpenCombineFoundation target as a product (#128)
This should fix `import OpenCombineFoundation` issue reported in #124.
2019-12-14 00:02:03 +00:00
Sergej Jaskiewicz c6536cf8d3 Implement URLSession.DataTaskPublisher (#127) 2019-12-13 16:44:03 +03:00
Sergej Jaskiewicz cf41c25cf7 Implement NotificationCenter.Publisher 2019-12-13 10:34:47 +03:00
Sergej Jaskiewicz b4557fb311 Create OpenCombineFoundation target
Implement TopLevelEncoder/TopLevelDecoder conformances for:

- JSONEncoder
- JSONDecoder
- PropertyListEncoder
- PropertyListDecoder
2019-12-13 10:34:47 +03:00
Sergej Jaskiewicz f8e6e66ab4 Fix integer overflows in DispatchQueue.SchedulerTimeType.Stride (#126) 2019-12-12 23:45:01 +03:00
Joe Spadafora 95b42abce3 Implement Publishers.ReplaceEmpty (#122) 2019-12-11 19:34:24 +03:00
Sergej Jaskiewicz 899a04bb3f Bump version to 0.7.0 2019-12-11 02:43:17 +03:00
Sergej Jaskiewicz 5f9a700689 Implement Publishers.Concatenate (#90) 2019-12-10 13:37:44 +03:00
Sergej Jaskiewicz a300fd09d3 [CocoaPods] Make COpenCombineHelpers part of the OpenCombine pod
CocoaPods doesn't support multiple Swift modules in the same pod.
Build COpenCombineHelpers sources together with OpenCombine sources
as a single module.

Previously COpenCombineHelpers was a separate pod. This was suboptimal,
as it made making changes in both targets very hard: you'd have to
push COpenCombineHelpers to trunk in order to pass validation.
2019-12-09 15:18:53 +03:00
Sergej Jaskiewicz 5973f86c6e Implement Publishers.HandleEvents 2019-12-09 15:18:53 +03:00
Sergej Jaskiewicz 1b5afdba26 Implement Publishers.Breakpoint 2019-12-09 15:18:53 +03:00
Sergej Jaskiewicz 51d5d1e71d Implement MeasureInterval (#117) 2019-12-03 14:26:00 +03:00
Sergej Jaskiewicz a8bc5cc046 Implement SubscribeOn (#116) 2019-12-03 12:11:31 +03:00
Sergej Jaskiewicz 86d6170dc9 Implement ReceiveOn (#115) 2019-12-02 20:30:58 +03:00
Sergej Jaskiewicz 171131d768 Implement Delay (#114) 2019-12-02 18:18:46 +03:00
Sergej Jaskiewicz d6b4fb4115 Bump COpenCombineHelpers.podspec version 2019-11-26 19:21:18 +03:00
94 changed files with 10778 additions and 1344 deletions
+22 -16
View File
@@ -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)"
-28
View File
@@ -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
View File
@@ -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
View File
@@ -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
+3 -3
View File
@@ -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
+25
View File
@@ -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
View File
@@ -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
+3 -3
View File
@@ -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
+163
View File
@@ -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"
+4 -2
View File
@@ -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 {
-2
View File
@@ -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,
+3 -2
View File
@@ -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.
+112 -5
View File
@@ -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 {}
+20
View File
@@ -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 publishers elements.
/// - Returns: A publisher that prefixes the specified elements prior to this
/// publishers 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 publishers
/// elements.
/// - Returns: A publisher that prefixes the sequence of elements prior to this
/// publishers 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 publishers output with the elements emitted by the given publisher.
///
/// The resulting publisher doesnt emit any elements until the prefixing publisher
/// finishes.
///
/// - Parameter publisher: The prefixing publisher.
/// - Returns: A publisher that prefixes the prefixing publishers elements prior to
/// this publishers 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 publishers output with the elements emitted by the given publisher.
///
/// This operator produces no elements until this publisher finishes. It then produces
/// this publishers elements, followed by the given publishers elements.
/// If this publisher fails with an error, the prefixing publisher does not publish
/// the provided publishers elements.
///
/// - Parameter publisher: The appending publisher.
/// - Returns: A publisher that appends the appending publishers elements after this
/// publishers 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 publishers 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 instances 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()
}
}
}
+16 -3
View File
@@ -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()
)
}
}
+3 -3
View File
@@ -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])
}
}
+4
View File
@@ -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))