24 Commits

Author SHA1 Message Date
Maximilian Wendel 915a7efaf5 Prepare for 0.10.1 (#185) 2020-10-04 15:01:09 +03:00
Sergej Jaskiewicz 024e576b0f Add link to generated interfaces for Combine 2020-10-01 13:21:33 +03:00
Max Desiatov f4a611e95f Run compatibility tests on iOS 13.6/Xcode 11.6 (#181) 2020-08-31 12:12:38 +01:00
Maximilian Wendel c09e47f792 Fix OperationQueue scheduler on non-Darwin platforms before Swift 5.1 (#177) 2020-07-29 16:26:50 +03:00
Maximilian Wendel dd6be33016 Don't use PropertyListEncoder on non-Darwin platforms before Swift 5.1 (#176)
PropertyListEncoder and PropertyListDecoder are both unavailable prior to Swift 5.1, causing a build error for Swift 5.0.
2020-07-29 16:24:28 +03:00
dependabot[bot] 5af4fb6ba4 Bump json from 2.2.0 to 2.3.1 (#175) 2020-07-28 07:22:03 +00:00
Adam Leonard 0ca4c7658f Fix a build error on linux: kCFStringEncodingUTF8 is not defined. (#173)
Instead, use `CFStringBuiltInEncodings.UTF8.rawValue`.

Also fix a type error I was getting in a unit test.

Co-authored-by: adaml <adam@seesaw.me>
2020-07-21 16:05:48 +03:00
Alexey Salangin 8cf59d6d2a Fix some typos (#172) 2020-07-14 08:48:35 +03:00
Sergej Jaskiewicz f3d068d6f2 Bump the version to 0.10.0 (#171) 2020-06-28 20:39:03 +03:00
Sergej Jaskiewicz 1cfb4a2eae Implement Publishers.Debounce (#133) 2020-06-28 19:50:45 +03:00
Sergej Jaskiewicz 2b64b7981d Implement Publishers.Timeout (#164) 2020-06-28 14:31:15 +03:00
Sergej Jaskiewicz ad95dfdc8c Update CircleCI badge 2020-06-26 17:25:12 +03:00
Sergej Jaskiewicz 988644159e Update badges after migrating to an organization 2020-06-26 16:32:22 +03:00
Sergej Jaskiewicz a9fa1ed4f4 Update the repository URL after migrating to an organization 2020-06-26 16:19:42 +03:00
Sergej Jaskiewicz 3f125b30e1 Implement OperationQueue scheduler (#165) 2020-06-26 15:40:15 +03:00
Sergej Jaskiewicz c9e7293a2a Fix behavior of CurrentValueSubject when setting new value after completion 2020-06-26 11:38:57 +03:00
Sergej Jaskiewicz f5d2c39c58 Add a test for CurrentValueSUbject 2020-06-26 11:17:32 +03:00
Sergej Jaskiewicz 70bf8e8bb3 Run compatibility tests on iOS 13.5/Xcode 11.5 2020-06-26 00:11:34 +03:00
Sergej Jaskiewicz f04053e1eb A more efficient and correct implementation of Future 2020-06-26 00:11:34 +03:00
Sergej Jaskiewicz af510706d7 A more efficient and correct implementation of CurrentValueSubject 2020-06-26 00:11:34 +03:00
Sergej Jaskiewicz 29fbf7de31 A more efficient and correct implementation of PassthroughSubject 2020-06-26 00:11:34 +03:00
Sergej Jaskiewicz 102eef88a0 Implement ConduitList 2020-06-26 00:11:34 +03:00
Sergej Jaskiewicz b34d4652d3 Make TimerPublisher tests more stable (#167) 2020-06-24 16:09:15 +03:00
Max Desiatov fcc2a4350a Add TimerPublisher and Timer.publish (#156)
Co-authored-by: Sergej Jaskiewicz <jaskiewiczs@icloud.com>
2020-06-23 20:55:20 +03:00
64 changed files with 4395 additions and 664 deletions
+8 -8
View File
@@ -63,35 +63,35 @@ jobs:
command: |
bash <(curl -s https://codecov.io/bash) -D DerivedData
"Execute compatibility tests on iOS 13.4 (Xcode 11.4.0, Swift 5.2.0)":
"Execute compatibility tests on iOS 13.6 (Xcode 11.6.0, Swift 5.2.4)":
macos:
xcode: "11.4.0"
xcode: "11.6.0"
environment:
SWIFT_VERSION: "5.2.0"
SWIFT_VERSION: "5.2.4"
steps:
- checkout
- run:
name: Generating Xcode project
command: make generate-compatibility-xcodeproj
- run:
name: Building for testing on iOS 13.4 with xcodebuild
name: Building for testing on iOS 13.6 with xcodebuild
command: |
set -o pipefail \
&& xcodebuild build-for-testing \
-scheme OpenCombine-Package \
-destination "platform=iOS Simulator,name=iPhone 11,OS=13.4" \
-destination "platform=iOS Simulator,name=iPhone 11,OS=13.6" \
-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.4 with xcodebuild
name: Testing against Combine on iOS 13.6 with xcodebuild
command: |
set -o pipefail \
&& xcodebuild test-without-building \
-scheme OpenCombine-Package \
-destination "platform=iOS Simulator,name=iPhone 11,OS=13.4" \
-destination "platform=iOS Simulator,name=iPhone 11,OS=13.6" \
-derivedDataPath DerivedData \
| tee xcodebuild_test-without-building.log \
| xcpretty --report junit -o build/reports/results.xml
@@ -259,7 +259,7 @@ workflows:
- "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.4 (Xcode 11.4.0, Swift 5.2.0)"
- "Execute compatibility tests on iOS 13.6 (Xcode 11.6.0, Swift 5.2.4)"
"OpenCombine: execute tests on iOS":
jobs:
- "Execute tests on iOS 9.3 (Xcode 10.2.1, Swift 5.0.1)"
+1 -1
View File
@@ -93,7 +93,7 @@ GEM
http-cookie (1.0.3)
domain_name (~> 0.5)
httpclient (2.8.3)
json (2.2.0)
json (2.3.1)
jwt (2.1.0)
memoist (0.16.1)
mime-types (3.3)
+1 -1
View File
@@ -1,6 +1,6 @@
Pod::Spec.new do |spec|
spec.name = "OpenCombine"
spec.version = "0.9.0"
spec.version = "0.10.1"
spec.summary = "Open source implementation of Apple's Combine framework for processing values over time."
spec.description = <<-DESC
+2 -2
View File
@@ -1,6 +1,6 @@
Pod::Spec.new do |spec|
spec.name = "OpenCombineDispatch"
spec.version = "0.9.0"
spec.version = "0.10.1"
spec.summary = "OpenCombine + Dispatch interoperability"
spec.description = <<-DESC
@@ -21,5 +21,5 @@ Pod::Spec.new do |spec|
spec.tvos.deployment_target = "9.0"
spec.source_files = "Sources/OpenCombineDispatch/**/*.swift"
spec.dependency "OpenCombine", '>= 0.8'
spec.dependency "OpenCombine", '>= 0.9'
end
+2 -2
View File
@@ -1,6 +1,6 @@
Pod::Spec.new do |spec|
spec.name = "OpenCombineFoundation"
spec.version = "0.9.0"
spec.version = "0.10.1"
spec.summary = "OpenCombine + OpenCombineFoundation interoperability"
spec.description = <<-DESC
@@ -21,5 +21,5 @@ Pod::Spec.new do |spec|
spec.tvos.deployment_target = "9.0"
spec.source_files = "Sources/OpenCombineFoundation/**/*.swift"
spec.dependency "OpenCombine", '>= 0.8'
spec.dependency "OpenCombine", '>= 0.9'
end
+11 -11
View File
@@ -1,6 +1,6 @@
# OpenCombine
[![CircleCI](https://circleci.com/gh/broadwaylamb/OpenCombine/tree/master.svg?style=svg)](https://circleci.com/gh/broadwaylamb/OpenCombine/tree/master)
[![codecov](https://codecov.io/gh/broadwaylamb/OpenCombine/branch/master/graph/badge.svg)](https://codecov.io/gh/broadwaylamb/OpenCombine)
[![OpenCombine](https://circleci.com/gh/OpenCombine/OpenCombine.svg?style=svg)](https://circleci.com/gh/OpenCombine/OpenCombine)
[![codecov](https://codecov.io/gh/OpenCombine/OpenCombine/branch/master/graph/badge.svg)](https://codecov.io/gh/OpenCombine/OpenCombine)
![Language](https://img.shields.io/badge/Swift-5.0-orange.svg)
![Platform](https://img.shields.io/badge/platform-Linux%20%7C%20macOS%20%7C%20iOS%20%7C%20watchOS%20%7C%20tvOS-lightgrey.svg)
![Cocoapods](https://img.shields.io/cocoapods/v/OpenCombine?color=blue)
@@ -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.9.0")
.package(url: "https://github.com/OpenCombine/OpenCombine.git", from: "0.10.1")
],
targets: [
.target(name: "MyAwesomePackage", dependencies: ["OpenCombine",
@@ -35,7 +35,7 @@ targets: [
###### Xcode
`OpenCombine` can also be added as a SPM dependency directly in your Xcode project *(requires Xcode 11 upwards)*.
To do so, open Xcode, use **File****Swift Packages****Add Package Dependency…**, enter the [repository URL](https://github.com/broadwaylamb/OpenCombine.git), choose the latest available version, and activate the checkboxes:
To do so, open Xcode, use **File****Swift Packages****Add Package Dependency…**, enter the [repository URL](https://github.com/OpenCombine/OpenCombine.git), choose the latest available version, and activate the checkboxes:
<p align="center">
<img alt="Select the OpenCombine and OpenCombineDispatch targets"
@@ -46,18 +46,18 @@ 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.9'
pod 'OpenCombineDispatch', '~> 0.9'
pod 'OpenCombineFoundation', '~> 0.9'
pod 'OpenCombine', '~> 0.10.1'
pod 'OpenCombineDispatch', '~> 0.10.1'
pod 'OpenCombineFoundation', '~> 0.10.1'
```
### Contributing
In order to work on this project you will need Xcode 10.2 and Swift 5.0 or later.
Please refer to the [issue #1](https://github.com/broadwaylamb/OpenCombine/issues/1) for the list of operators that remain unimplemented, as well as the [RemainingCombineInterface.swift](https://github.com/broadwaylamb/OpenCombine/blob/master/RemainingCombineInterface.swift) file. The latter contains the generated interface of Apple's Combine from the latest Xcode 11 version. When the functionality is implemented in OpenCombine, it should be removed from the RemainingCombineInterface.swift file.
Please refer to the [issue #1](https://github.com/OpenCombine/OpenCombine/issues/1) for the list of operators that remain unimplemented, as well as the [RemainingCombineInterface.swift](https://github.com/OpenCombine/OpenCombine/blob/master/RemainingCombineInterface.swift) file. The latter contains the generated interface of Apple's Combine from the latest Xcode 11 version. When the functionality is implemented in OpenCombine, it should be removed from the RemainingCombineInterface.swift file.
You can refer to [this gist](https://gist.github.com/broadwaylamb/c2c8550d76b3ff851c4c1dbf0a872e26) to observe Apple's Combine API changes between different Xcode (beta) versions, or to [this gist](https://gist.github.com/broadwaylamb/82dc2ce4ffbe06527c2c352b8f10910f) to see the relevant contents of the .swiftinterface file for Combine.
You can refer to [this repo](https://github.com/OpenCombine/combine-interfaces) to observe Apple's Combine API and documentation changes between different Xcode (beta) versions.
You can run compatibility tests against Apple's Combine. In order to do that you will need either macOS 10.14 with iOS 13 simulator installed (since the only way we can get Apple's Combine on macOS 10.14 is using the simulator), or macOS 10.15 (Apple's Combine is bundled with the OS). Execute the following command from the root of the package:
@@ -67,7 +67,7 @@ $ make test-compatibility
Or enable the `-DOPENCOMBINE_COMPATIBILITY_TEST` compiler flag in Xcode's build settings. Note that on iOS only the latter will work.
> NOTE: Before starting to work on some feature, please consult the [GitHub project](https://github.com/broadwaylamb/OpenCombine/projects/2) to make sure that nobody's already making progress on the same feature! If not, then please create a draft PR to indicate that you're beginning your work.
> NOTE: Before starting to work on some feature, please consult the [GitHub project](https://github.com/OpenCombine/OpenCombine/projects/2) to make sure that nobody's already making progress on the same feature! If not, then please create a draft PR to indicate that you're beginning your work.
#### Releasing a new version
@@ -76,7 +76,7 @@ Or enable the `-DOPENCOMBINE_COMPATIBILITY_TEST` compiler flag in Xcode's build
1. Bump the version in `OpenCombine.podspec`, `OpenCombineDispatch.podspec` and `OpenCombineFoundation.podspec`. In the latter two you will also need to set the `spec.dependency "OpenCombine"` property to the **previous** version. Why? Because otherwise the `pod lib lint` command that we run on our regular CI will fail when validating the `OpenCombineDispatch` and `OpenCombineFoundation` podspecs, since the dependencies are not yet in the trunk. If we set the dependencies to the previous version (which is already in the trunk), everything will be fine. This is purely to make the CI work. The clients will not experience any issues, since the version is specified as `>=`.
1. Create a pull request to master for the release branch and make sure the CI passes.
1. Merge the pull request.
1. In the GitHub web interface on the [releases](https://github.com/broadwaylamb/OpenCombine/releases) page, click the **Draft a new release** button.
1. In the GitHub web interface on the [releases](https://github.com/OpenCombine/OpenCombine/releases) page, click the **Draft a new release** button.
1. The **Tag version** and **Release title** fields should be filled with the version number.
1. The description of the release should be consistent with the previous releases. It is a good practice to divide the description into several sections: additions, bugfixes, known issues etc. Also, be sure to mention the nicknames of the contributors of the new release.
1. Publish the release.
-97
View File
@@ -760,103 +760,6 @@ extension Publisher {
public func throttle<S>(for interval: S.SchedulerTimeType.Stride, scheduler: S, latest: Bool) -> Publishers.Throttle<Self, S> where S : Scheduler
}
extension Publishers {
/// A publisher that publishes elements only after a specified time interval elapses between events.
public struct Debounce<Upstream, Context> : Publisher where Upstream : Publisher, Context : Scheduler {
/// The kind of values published by this publisher.
public typealias Output = Upstream.Output
/// The kind of errors this publisher might publish.
///
/// Use `Never` if this `Publisher` does not publish errors.
public typealias Failure = Upstream.Failure
/// The publisher from which this publisher receives elements.
public let upstream: Upstream
/// The amount of time the publisher should wait before publishing an element.
public let dueTime: Context.SchedulerTimeType.Stride
/// The scheduler on which this publisher delivers elements.
public let scheduler: Context
/// Scheduler options that customize this publishers delivery of elements.
public let options: Context.SchedulerOptions?
public init(upstream: Upstream, dueTime: Context.SchedulerTimeType.Stride, scheduler: Context, options: Context.SchedulerOptions?)
/// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)`
///
/// - SeeAlso: `subscribe(_:)`
/// - Parameters:
/// - subscriber: The subscriber to attach to this `Publisher`.
/// once attached it can begin to receive values.
public func receive<S>(subscriber: S) where S : Subscriber, Upstream.Failure == S.Failure, Upstream.Output == S.Input
}
}
extension Publisher {
/// Publishes elements only after a specified time interval elapses between events.
///
/// Use this operator when you want to wait for a pause in the delivery of events from the upstream publisher. For example, call `debounce` on the publisher from a text field to only receive elements when the user pauses or stops typing. When they start typing again, the `debounce` holds event delivery until the next pause.
/// - Parameters:
/// - dueTime: The time the publisher should wait before publishing an element.
/// - scheduler: The scheduler on which this publisher delivers elements
/// - options: Scheduler options that customize this publishers delivery of elements.
/// - Returns: A publisher that publishes events only after a specified time elapses.
public func debounce<S>(for dueTime: S.SchedulerTimeType.Stride, scheduler: S, options: S.SchedulerOptions? = nil) -> Publishers.Debounce<Self, S> where S : Scheduler
}
extension Publishers {
public struct Timeout<Upstream, Context> : Publisher where Upstream : Publisher, Context : Scheduler {
/// The kind of values published by this publisher.
public typealias Output = Upstream.Output
/// The kind of errors this publisher might publish.
///
/// Use `Never` if this `Publisher` does not publish errors.
public typealias Failure = Upstream.Failure
public let upstream: Upstream
public let interval: Context.SchedulerTimeType.Stride
public let scheduler: Context
public let options: Context.SchedulerOptions?
public let customError: (() -> Upstream.Failure)?
public init(upstream: Upstream, interval: Context.SchedulerTimeType.Stride, scheduler: Context, options: Context.SchedulerOptions?, customError: (() -> Publishers.Timeout<Upstream, Context>.Failure)?)
/// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)`
///
/// - SeeAlso: `subscribe(_:)`
/// - Parameters:
/// - subscriber: The subscriber to attach to this `Publisher`.
/// once attached it can begin to receive values.
public func receive<S>(subscriber: S) where S : Subscriber, Upstream.Failure == S.Failure, Upstream.Output == S.Input
}
}
extension Publisher {
/// Terminates publishing if the upstream publisher exceeds the specified time interval without producing an element.
///
/// - Parameters:
/// - interval: The maximum time interval the publisher can go without emitting an element, expressed in the time system of the scheduler.
/// - scheduler: The scheduler to deliver events on.
/// - options: Scheduler options that customize the delivery of elements.
/// - customError: A closure that executes if the publisher times out. The publisher sends the failure returned by this closure to the subscriber as the reason for termination.
/// - Returns: A publisher that terminates if the specified interval elapses with no events received from the upstream publisher.
public func timeout<S>(_ interval: S.SchedulerTimeType.Stride, scheduler: S, options: S.SchedulerOptions? = nil, customError: (() -> Self.Failure)? = nil) -> Publishers.Timeout<Self, S> where S : Scheduler
}
extension Publishers {
/// A publisher created by applying the zip function to two upstream publishers.
-70
View File
@@ -37,73 +37,3 @@ extension NSObject.KeyValueObservingPublisher : Combine.Publisher {
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
}
}
+2 -2
View File
@@ -62,8 +62,8 @@ public struct AnySubscriber<Input, Failure: Error>: Subscriber,
if let playgroundDescription = subscriber as? CustomPlaygroundDisplayConvertible {
playgroundDescriptionThunk = { playgroundDescription.playgroundDescription }
} else if let desccription = subscriber as? CustomStringConvertible {
playgroundDescriptionThunk = { desccription.description }
} else if let description = subscriber as? CustomStringConvertible {
playgroundDescriptionThunk = { description.description }
} else {
let fixedDescription = String(describing: type(of: subscriber))
playgroundDescriptionThunk = { fixedDescription }
+179 -85
View File
@@ -9,26 +9,29 @@
/// changes.
public final class CurrentValueSubject<Output, Failure: Error>: Subject {
private let _lock = UnfairRecursiveLock.allocate()
private let lock = UnfairLock.allocate()
// TODO: Combine uses bag data structure
private var _subscriptions: [Conduit] = []
private var active = true
private var _value: Output
private var completion: Subscribers.Completion<Failure>?
private var _completion: Subscribers.Completion<Failure>?
private var downstreams = ConduitList<Output, Failure>.empty
internal var upstreamSubscriptions: [Subscription] = []
private var currentValue: Output
internal var hasAnyDownstreamDemand = false
private var upstreamSubscriptions: [Subscription] = []
/// The value wrapped by this subject, published as a new element whenever it changes.
public var value: Output {
get {
return _value
lock.lock()
defer { lock.unlock() }
return currentValue
}
set {
send(newValue)
lock.lock()
currentValue = newValue
sendValueAndConsumeLock(newValue)
}
}
@@ -36,122 +39,213 @@ public final class CurrentValueSubject<Output, Failure: Error>: Subject {
///
/// - Parameter value: The initial value to publish.
public init(_ value: Output) {
self._value = value
self.currentValue = value
}
deinit {
for subscription in _subscriptions {
subscription._downstream = nil
for subscription in upstreamSubscriptions {
subscription.cancel()
}
_lock.deallocate()
lock.deallocate()
}
public func send(subscription: Subscription) {
_lock.do {
upstreamSubscriptions.append(subscription)
subscription.request(.unlimited)
}
lock.lock()
upstreamSubscriptions.append(subscription)
lock.unlock()
subscription.request(.unlimited)
}
public func receive<Subscriber: OpenCombine.Subscriber>(subscriber: Subscriber)
where Output == Subscriber.Input, Failure == Subscriber.Failure
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Output == Downstream.Input, Failure == Downstream.Failure
{
_lock.do {
if let completion = _completion {
subscriber.receive(subscription: Subscriptions.empty)
subscriber.receive(completion: completion)
return
} else {
let subscription = Conduit(parent: self,
downstream: AnySubscriber(subscriber))
_subscriptions.append(subscription)
subscriber.receive(subscription: subscription)
}
lock.lock()
if active {
let conduit = Conduit(parent: self, downstream: subscriber)
downstreams.insert(conduit)
lock.unlock()
subscriber.receive(subscription: conduit)
} else {
let completion = self.completion!
lock.unlock()
subscriber.receive(subscription: Subscriptions.empty)
subscriber.receive(completion: completion)
}
}
public func send(_ input: Output) {
_lock.do {
_value = input
for subscription in _subscriptions where !subscription.isCompleted {
if subscription._demand > 0 {
subscription._offer(input)
subscription._demand -= 1
} else {
subscription._delivered = false
}
}
lock.lock()
sendValueAndConsumeLock(input)
}
private func sendValueAndConsumeLock(_ newValue: Output) {
#if DEBUG
lock.assertOwner()
#endif
guard active else {
lock.unlock()
return
}
currentValue = newValue
let downstreams = self.downstreams
lock.unlock()
downstreams.forEach { conduit in
conduit.offer(newValue)
}
}
public func send(completion: Subscribers.Completion<Failure>) {
_completion = completion
_lock.do {
for subscriber in _subscriptions {
subscriber._receive(completion: completion)
}
lock.lock()
guard active else {
lock.unlock()
return
}
active = false
self.completion = completion
let downstreams = self.downstreams
self.downstreams.removeAll()
lock.unlock()
downstreams.forEach { conduit in
conduit.finish(completion: completion)
}
}
private func disassociate(_ conduit: ConduitBase<Output, Failure>) {
lock.lock()
guard active else {
lock.unlock()
return
}
downstreams.remove(conduit)
lock.unlock()
}
}
extension CurrentValueSubject {
fileprivate class Conduit: Subscription {
private final class Conduit<Downstream: Subscriber>
: ConduitBase<Output, Failure>,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
where Downstream.Input == Output, Downstream.Failure == Failure
{
fileprivate var _parent: CurrentValueSubject?
fileprivate var parent: CurrentValueSubject?
fileprivate var _downstream: AnySubscriber<Output, Failure>?
fileprivate var downstream: Downstream?
fileprivate var _demand: Subscribers.Demand = .none
fileprivate var demand = Subscribers.Demand.none
/// Whethere we satisfied the demand
fileprivate var _delivered = false
private var lock = UnfairLock.allocate()
var isCompleted: Bool {
return _parent == nil
}
private var downstreamLock = UnfairRecursiveLock.allocate()
fileprivate func _offer(_ value: Output) {
let newDemand = _downstream?.receive(value) ?? .none
_demand += newDemand
_delivered = true
}
private var deliveredCurrentValue = false
fileprivate init(parent: CurrentValueSubject,
downstream: AnySubscriber<Output, Failure>) {
_parent = parent
_downstream = downstream
downstream: Downstream) {
self.parent = parent
self.downstream = downstream
}
fileprivate func _receive(completion: Subscribers.Completion<Failure>) {
if !isCompleted {
_parent = nil
_downstream?.receive(completion: completion)
deinit {
lock.deallocate()
downstreamLock.deallocate()
}
override func offer(_ output: Output) {
lock.lock()
guard demand > 0, let downstream = self.downstream else {
deliveredCurrentValue = false
lock.unlock()
return
}
demand -= 1
deliveredCurrentValue = true
lock.unlock()
downstreamLock.lock()
let newDemand = downstream.receive(output)
downstreamLock.unlock()
guard newDemand > 0 else { return }
lock.lock()
demand += newDemand
lock.unlock()
}
func request(_ demand: Subscribers.Demand) {
precondition(demand > 0)
_parent?._lock.do {
if !_delivered, let value = _parent?.value {
_offer(value)
_demand += demand
_demand -= 1
} else {
_demand = demand
}
_parent?.hasAnyDownstreamDemand = true
override func finish(completion: Subscribers.Completion<Failure>) {
lock.lock()
guard let downstream = self.downstream else {
lock.unlock()
return
}
self.downstream = nil
let parent = self.parent
self.parent = nil
lock.unlock()
parent?.disassociate(self)
downstreamLock.lock()
downstream.receive(completion: completion)
downstreamLock.unlock()
}
func cancel() {
_parent = nil
override func request(_ demand: Subscribers.Demand) {
demand.assertNonZero()
lock.lock()
guard let downstream = self.downstream else {
lock.unlock()
return
}
if deliveredCurrentValue {
self.demand += demand
lock.unlock()
return
}
// Hasn't yet delivered the current value
self.demand += demand
deliveredCurrentValue = true
if let currentValue = self.parent?.value {
self.demand -= 1
lock.unlock()
downstreamLock.lock()
let newDemand = downstream.receive(currentValue)
downstreamLock.unlock()
guard newDemand > 0 else { return }
lock.lock()
self.demand += newDemand
}
lock.unlock()
}
override func cancel() {
lock.lock()
if self.downstream == nil {
lock.unlock()
return
}
self.downstream = nil
let parent = self.parent
self.parent = nil
lock.unlock()
parent?.disassociate(self)
}
var description: String { return "CurrentValueSubject" }
var customMirror: Mirror {
lock.lock()
defer { lock.unlock() }
let children: [Mirror.Child] = [
("parent", parent as Any),
("downstream", downstream as Any),
("demand", demand),
("subject", parent as Any)
]
return Mirror(self, children: children)
}
var playgroundDescription: Any { return description }
}
}
extension CurrentValueSubject.Conduit: CustomStringConvertible {
fileprivate var description: String { return "CurrentValueSubject" }
}
+153 -69
View File
@@ -6,113 +6,197 @@
//
/// A publisher that eventually produces one value and then finishes or fails.
public final class Future<Output, Failure>: Publisher where Failure: Error {
public final class Future<Output, Failure: Error>: Publisher {
public typealias Promise = (Result<Output, Failure>) -> Void
private let _lock = UnfairRecursiveLock.allocate()
private var _subscriptions: [Conduit] = []
private let lock = UnfairLock.allocate()
private var downstreams = ConduitList<Output, Failure>.empty
private var result: Result<Output, Failure>?
public init(
_ attemptToFulfill: @escaping (@escaping Promise) -> Void
) {
attemptToFulfill { result in
self._lock.do {
guard self.result == nil else { return }
self.result = result
self._publish(result)
}
}
attemptToFulfill(self.promise)
}
deinit {
_lock.deallocate()
lock.deallocate()
}
/// This function is called to attach the specified `Subscriber` to this
/// `Publisher` by `subscribe(_:)`
///
/// - SeeAlso: `subscribe(_:)`
/// - Parameters:
/// - subscriber: The subscriber to attach to this `Publisher`.
/// once attached it can begin to receive values.
public func receive<Downstream: Subscriber>(
subscriber: Downstream
) where Output == Downstream.Input, Failure == Downstream.Failure {
let subscription = Conduit(parent: self,
downstream: AnySubscriber(subscriber))
_subscriptions.append(subscription)
subscriber.receive(subscription: subscription)
}
private func _acknowledgeDownstreamDemand() {
_lock.do {
guard let result = result else { return }
_publish(result)
private func promise(_ result: Result<Output, Failure>) {
lock.lock()
guard self.result == nil else {
lock.unlock()
return
}
self.result = result
let downstreams = self.downstreams
self.downstreams.removeAll()
lock.unlock()
switch result {
case .success(let output):
downstreams.forEach { $0.offer(output) }
case .failure(let error):
downstreams.forEach { $0.finish(completion: .failure(error)) }
}
}
private func _publish(_ result: Result<Output, Failure>) {
for subscription in self._subscriptions where !subscription._isCompleted {
switch result {
case let .success(output) where subscription._demand > 0:
subscription._demand -= 1
subscription._demand += subscription._downstream?.receive(output) ?? .none
subscription._receive(completion: .finished)
case let .failure(error):
subscription._receive(completion: .failure(error))
// nothing to do if no demand
default: ()
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Output == Downstream.Input, Failure == Downstream.Failure
{
let conduit = Conduit(parent: self, downstream: subscriber)
lock.lock()
if let result = self.result {
downstreams.insert(conduit)
lock.unlock()
subscriber.receive(subscription: conduit)
conduit.fulfill(result)
} else {
downstreams.insert(conduit)
lock.unlock()
subscriber.receive(subscription: conduit)
}
}
private func disassociate(_ conduit: ConduitBase<Output, Failure>) {
lock.lock()
downstreams.remove(conduit)
lock.unlock()
}
}
extension Future {
fileprivate final class Conduit: Subscription {
private final class Conduit<Downstream: Subscriber>
: ConduitBase<Output, Failure>,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
where Downstream.Input == Output, Downstream.Failure == Failure
{
fileprivate var _parent: Future<Output, Failure>?
fileprivate var parent: Future?
fileprivate var _downstream: AnySubscriber<Output, Failure>?
fileprivate var downstream: Downstream?
fileprivate var _demand: Subscribers.Demand = .none
fileprivate var hasAnyDemand = false
fileprivate var _isCompleted: Bool {
return _parent == nil
private var lock = UnfairLock.allocate()
private var downstreamLock = UnfairRecursiveLock.allocate()
fileprivate init(parent: Future, downstream: Downstream) {
self.parent = parent
self.downstream = downstream
}
fileprivate init(parent: Future<Output, Failure>,
downstream: AnySubscriber<Output, Failure>) {
_parent = parent
_downstream = downstream
deinit {
lock.deallocate()
downstreamLock.deallocate()
}
fileprivate func _receive(completion: Subscribers.Completion<Failure>) {
if !_isCompleted {
_parent = nil
_downstream?.receive(completion: completion)
fileprivate func fulfill(_ result: Result<Output, Failure>) {
lock.lock()
guard let downstream = self.downstream else {
lock.unlock()
return
}
let parent = self.parent
if case .success = result, !hasAnyDemand {
lock.unlock()
return
}
self.downstream = nil
self.parent = nil
lock.unlock()
downstreamLock.lock()
switch result {
case .success(let output):
_ = downstream.receive(output)
downstream.receive(completion: .finished)
case .failure(let error):
downstream.receive(completion: .failure(error))
}
downstreamLock.unlock()
parent?.disassociate(self)
}
override func offer(_ output: Output) {
fulfill(.success(output))
}
override func finish(completion: Subscribers.Completion<Failure>) {
switch completion {
case .finished:
assertionFailure("unreachable")
case .failure(let error):
fulfill(.failure(error))
}
}
fileprivate func request(_ demand: Subscribers.Demand) {
override func request(_ demand: Subscribers.Demand) {
demand.assertNonZero()
_parent?._lock.do {
_demand += demand
lock.lock()
guard let downstream = self.downstream, let parent = self.parent else {
lock.unlock()
return
}
_parent?._acknowledgeDownstreamDemand()
hasAnyDemand = true
parent.lock.lock()
guard let result = parent.result else {
parent.lock.unlock()
lock.unlock()
return
}
parent.lock.unlock()
self.downstream = nil
self.parent = nil
lock.unlock()
downstreamLock.lock()
switch result {
case .success(let output):
_ = downstream.receive(output)
downstream.receive(completion: .finished)
case .failure(let error):
// This branch is not reachable under normal circumstances,
// but may be reachable in case of a race condition.
downstream.receive(completion: .failure(error))
}
downstreamLock.unlock()
parent.disassociate(self)
}
fileprivate func cancel() {
_parent = nil
override func cancel() {
lock.lock()
if self.downstream == nil {
lock.unlock()
return
}
self.downstream = nil
let parent = self.parent
self.parent = nil
lock.unlock()
parent?.disassociate(self)
}
var description: String { return "Future" }
var customMirror: Mirror {
lock.lock()
defer { lock.unlock() }
let children: [Mirror.Child] = [
("parent", parent as Any),
("downstream", downstream as Any),
("hasAnyDemand", hasAnyDemand),
("subject", parent as Any)
]
return Mirror(self, children: children)
}
var playgroundDescription: Any { return description }
}
}
extension Future.Conduit: CustomStringConvertible {
fileprivate var description: String { return "Future" }
}
@@ -0,0 +1,40 @@
//
// ConduitBase.swift
//
//
// Created by Sergej Jaskiewicz on 25.06.2020.
//
internal class ConduitBase<Output, Failure: Error>: Subscription {
internal init() {}
internal func offer(_ output: Output) {
abstractMethod()
}
internal func finish(completion: Subscribers.Completion<Failure>) {
abstractMethod()
}
internal func request(_ demand: Subscribers.Demand) {
abstractMethod()
}
internal func cancel() {
abstractMethod()
}
}
extension ConduitBase: Equatable {
internal static func == (lhs: ConduitBase<Output, Failure>,
rhs: ConduitBase<Output, Failure>) -> Bool {
return ObjectIdentifier(lhs) == ObjectIdentifier(rhs)
}
}
extension ConduitBase: Hashable {
internal func hash(into hasher: inout Hasher) {
hasher.combine(ObjectIdentifier(self))
}
}
@@ -0,0 +1,57 @@
//
// ConduitList.swift
//
//
// Created by Sergej Jaskiewicz on 25.06.2020.
//
internal enum ConduitList<Output, Failure: Error> {
case empty
case single(ConduitBase<Output, Failure>)
case many(Set<ConduitBase<Output, Failure>>)
}
extension ConduitList {
internal mutating func insert(_ conduit: ConduitBase<Output, Failure>) {
switch self {
case .empty:
self = .single(conduit)
case .single(conduit):
break // This element already exists.
case .single(let existingConduit):
self = .many([existingConduit, conduit])
case .many(var set):
set.insert(conduit)
self = .many(set)
}
}
internal func forEach(
_ body: (ConduitBase<Output, Failure>) throws -> Void
) rethrows {
switch self {
case .empty:
break
case .single(let conduit):
try body(conduit)
case .many(let set):
try set.forEach(body)
}
}
internal mutating func remove(_ conduit: ConduitBase<Output, Failure>) {
switch self {
case .single(conduit):
self = .empty
case .empty, .single:
break
case .many(var set):
set.remove(conduit)
self = .many(set)
}
}
internal mutating func removeAll() {
self = .empty
}
}
@@ -9,7 +9,7 @@
///
/// Filter-like operators send an instance of their `Inner` class that is subclass
/// of this class to the upstream publisher (as subscriber) and
/// to the downstream subcriber (as subscription).
/// to the downstream subscriber (as subscription).
///
/// Filter-like operators include `Publishers.Filter`,
/// `Publishers.RemoveDuplicates`, `Publishers.PrefixWhile` and more.
-10
View File
@@ -11,13 +11,3 @@ import COpenCombineHelpers
internal typealias UnfairLock = __UnfairLock
internal typealias UnfairRecursiveLock = __UnfairRecursiveLock
extension UnfairRecursiveLock {
@inlinable
internal func `do`<Result>(_ body: () throws -> Result) rethrows -> Result {
lock()
defer { unlock() }
return try body()
}
}
@@ -9,7 +9,7 @@
///
/// Reduce-like operators send an instance of their `Inner` class that is subclass
/// of this class to the upstream publisher (as subscriber) and
/// to the downstream subcriber (as subsription).
/// to the downstream subscriber (as subscription).
///
/// Reduce-like operators include `Publishers.Reduce`, `Publishers.TryReduce`,
/// `Publishers.Count`, `Publishers.FirstWhere`, `Publishers.AllSatisfy` and more.
@@ -10,3 +10,14 @@ internal enum SubscriptionStatus {
case subscribed(Subscription)
case terminal
}
extension SubscriptionStatus {
internal var isAwaitingSubscription: Bool {
switch self {
case .awaitingSubscription:
return true
default:
return false
}
}
}
+155 -69
View File
@@ -9,14 +9,15 @@
///
/// Use a `PassthroughSubject` in unit tests when you want a publisher than can publish
/// specific values on-demand during tests.
public final class PassthroughSubject<Output, Failure: Error>: Subject {
public final class PassthroughSubject<Output, Failure: Error>: Subject {
private let _lock = UnfairRecursiveLock.allocate()
private let lock = UnfairLock.allocate()
private var _completion: Subscribers.Completion<Failure>?
private var active = true
// TODO: Combine uses bag data structure
private var _subscriptions: [Conduit] = []
private var completion: Subscribers.Completion<Failure>?
private var downstreams = ConduitList<Output, Failure>.empty
internal var upstreamSubscriptions: [Subscription] = []
@@ -25,112 +26,197 @@ public final class PassthroughSubject<Output, Failure: Error>: Subject {
public init() {}
deinit {
for subscription in _subscriptions {
subscription._downstream = nil
for subscription in upstreamSubscriptions {
subscription.cancel()
}
_lock.deallocate()
lock.deallocate()
}
public func send(subscription: Subscription) {
_lock.do {
upstreamSubscriptions.append(subscription)
if hasAnyDownstreamDemand {
subscription.request(.unlimited)
}
lock.lock()
upstreamSubscriptions.append(subscription)
let hasAnyDownstreamDemand = self.hasAnyDownstreamDemand
lock.unlock()
if hasAnyDownstreamDemand {
subscription.request(.unlimited)
}
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Output == Downstream.Input, Failure == Downstream.Failure
{
_lock.do {
if let completion = _completion {
subscriber.receive(subscription: Subscriptions.empty)
subscriber.receive(completion: completion)
return
} else {
let subscription = Conduit(parent: self,
downstream: AnySubscriber(subscriber))
_subscriptions.append(subscription)
subscriber.receive(subscription: subscription)
}
lock.lock()
if active {
let conduit = Conduit(parent: self, downstream: subscriber)
downstreams.insert(conduit)
lock.unlock()
subscriber.receive(subscription: conduit)
} else {
let completion = self.completion!
lock.unlock()
subscriber.receive(subscription: Subscriptions.empty)
subscriber.receive(completion: completion)
}
}
public func send(_ input: Output) {
_lock.do {
for subscription in _subscriptions
where !subscription._isCompleted && subscription._demand > 0
{
let newDemand = subscription._downstream?.receive(input) ?? .none
subscription._demand += newDemand
subscription._demand -= 1
}
lock.lock()
guard active else {
lock.unlock()
return
}
let downstreams = self.downstreams
lock.unlock()
downstreams.forEach { conduit in
conduit.offer(input)
}
}
public func send(completion: Subscribers.Completion<Failure>) {
_lock.do {
_completion = completion
for subscriber in _subscriptions {
subscriber._receive(completion: completion)
}
lock.lock()
guard active else {
lock.unlock()
return
}
active = false
self.completion = completion
let downstreams = self.downstreams
self.downstreams.removeAll()
lock.unlock()
downstreams.forEach { conduit in
conduit.finish(completion: completion)
}
}
private func _acknowledgeDownstreamDemand() {
_lock.do {
guard !hasAnyDownstreamDemand else { return }
hasAnyDownstreamDemand = true
for subscription in upstreamSubscriptions {
subscription.request(.unlimited)
}
private func acknowledgeDownstreamDemand() {
lock.lock()
if hasAnyDownstreamDemand {
lock.unlock()
return
}
hasAnyDownstreamDemand = true
let upstreamSubscriptions = self.upstreamSubscriptions
lock.unlock()
for subscription in upstreamSubscriptions {
subscription.request(.unlimited)
}
}
private func disassociate(_ conduit: ConduitBase<Output, Failure>) {
lock.lock()
guard active else {
lock.unlock()
return
}
downstreams.remove(conduit)
lock.unlock()
}
}
extension PassthroughSubject {
fileprivate final class Conduit: Subscription {
private final class Conduit<Downstream: Subscriber>
: ConduitBase<Output, Failure>,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
where Downstream.Input == Output, Downstream.Failure == Failure
{
fileprivate var _parent: PassthroughSubject?
fileprivate var parent: PassthroughSubject?
fileprivate var _downstream: AnySubscriber<Output, Failure>?
fileprivate var downstream: Downstream?
fileprivate var _demand: Subscribers.Demand = .none
fileprivate var demand = Subscribers.Demand.none
fileprivate var _isCompleted: Bool {
return _parent == nil
}
private var lock = UnfairLock.allocate()
private var downstreamLock = UnfairRecursiveLock.allocate()
fileprivate init(parent: PassthroughSubject,
downstream: AnySubscriber<Output, Failure>) {
_parent = parent
_downstream = downstream
downstream: Downstream) {
self.parent = parent
self.downstream = downstream
}
fileprivate func _receive(completion: Subscribers.Completion<Failure>) {
if !_isCompleted {
_parent = nil
_downstream?.receive(completion: completion)
deinit {
lock.deallocate()
downstreamLock.deallocate()
}
override func offer(_ output: Output) {
lock.lock()
guard demand > 0, let downstream = self.downstream else {
lock.unlock()
return
}
demand -= 1
lock.unlock()
downstreamLock.lock()
let newDemand = downstream.receive(output)
downstreamLock.unlock()
guard newDemand > 0 else { return }
lock.lock()
demand += newDemand
lock.unlock()
}
fileprivate func request(_ demand: Subscribers.Demand) {
override func finish(completion: Subscribers.Completion<Failure>) {
lock.lock()
guard let downstream = self.downstream else {
lock.unlock()
return
}
self.downstream = nil
let parent = self.parent
self.parent = nil
lock.unlock()
parent?.disassociate(self)
downstreamLock.lock()
downstream.receive(completion: completion)
downstreamLock.unlock()
}
override func request(_ demand: Subscribers.Demand) {
demand.assertNonZero()
_parent?._lock.do {
_demand += demand
lock.lock()
if self.downstream == nil {
lock.unlock()
return
}
_parent?._acknowledgeDownstreamDemand()
self.demand += demand
let parent = self.parent
lock.unlock()
parent?.acknowledgeDownstreamDemand()
}
fileprivate func cancel() {
_parent = nil
override func cancel() {
lock.lock()
if self.downstream == nil {
lock.unlock()
return
}
self.downstream = nil
let parent = self.parent
self.parent = nil
lock.unlock()
parent?.disassociate(self)
}
var description: String { return "PassthroughSubject" }
var customMirror: Mirror {
lock.lock()
defer { lock.unlock() }
let children: [Mirror.Child] = [
("parent", parent as Any),
("downstream", downstream as Any),
("demand", demand),
("subject", parent as Any)
]
return Mirror(self, children: children)
}
var playgroundDescription: Any { return description }
}
}
extension PassthroughSubject.Conduit: CustomStringConvertible {
fileprivate var description: String { return "PassthroughSubject" }
}
@@ -235,7 +235,7 @@ extension Optional.OCombine.Publisher {
in range: RangeExpression
) -> Optional<Output>.OCombine.Publisher where RangeExpression.Bound == Int {
let range = range.relative(to: 0 ..< Int.max)
precondition(range.lowerBound >= 0, "lowerBould must not be negative")
precondition(range.lowerBound >= 0, "lowerBound must not be negative")
// I don't know why, but Combine has this precondition
precondition(range.upperBound < .max - 1)
@@ -85,7 +85,7 @@ extension Publisher {
extension Publishers {
/// A publisher that emits all of one publishers elements before those from anothe
/// A publisher that emits all of one publishers elements before those from another
/// publisher.
public struct Concatenate<Prefix: Publisher, Suffix: Publisher>: Publisher
where Prefix.Failure == Suffix.Failure, Prefix.Output == Suffix.Output
@@ -0,0 +1,264 @@
//
// Publishers.Debounce.swift
//
//
// Created by Sergej Jaskiewicz on 17.12.2019.
//
extension Publisher {
/// Publishes elements only after a specified time interval elapses between events.
///
/// Use this operator when you want to wait for a pause in the delivery of events from
/// the upstream publisher. For example, call `debounce` on the publisher from a text
/// field to only receive elements when the user pauses or stops typing. When they
/// start typing again, the `debounce` holds event delivery until the next pause.
///
/// - Parameters:
/// - dueTime: The time the publisher should wait before publishing an element.
/// - scheduler: The scheduler on which this publisher delivers elements
/// - options: Scheduler options that customize this publishers delivery
/// of elements.
/// - Returns: A publisher that publishes events only after a specified time elapses.
public func debounce<Context: Scheduler>(
for dueTime: Context.SchedulerTimeType.Stride,
scheduler: Context,
options: Context.SchedulerOptions? = nil
) -> Publishers.Debounce<Self, Context> {
return .init(upstream: self,
dueTime: dueTime,
scheduler: scheduler,
options: options)
}
}
extension Publishers {
/// A publisher that publishes elements only after a specified time interval elapses
/// between events.
public struct Debounce<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 amount of time the publisher should wait before publishing an element.
public let dueTime: Context.SchedulerTimeType.Stride
/// The scheduler on which this publisher delivers elements.
public let scheduler: Context
/// Scheduler options that customize this publishers delivery of elements.
public let options: Context.SchedulerOptions?
public init(upstream: Upstream,
dueTime: Context.SchedulerTimeType.Stride,
scheduler: Context,
options: Context.SchedulerOptions?) {
self.upstream = upstream
self.dueTime = dueTime
self.scheduler = scheduler
self.options = options
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Downstream.Failure == Failure, Downstream.Input == Output
{
let inner = Inner(downstream: subscriber,
dueTime: dueTime,
scheduler: scheduler,
options: options)
upstream.subscribe(inner)
}
}
}
extension Publishers.Debounce {
private final class Inner<Downstream: Subscriber>
: Subscriber,
Subscription,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
where Upstream.Output == Downstream.Input,
Upstream.Failure == Downstream.Failure
{
typealias Input = Upstream.Output
typealias Failure = Upstream.Failure
private typealias Generation = UInt64
private let lock = UnfairLock.allocate()
private let downstreamLock = UnfairRecursiveLock.allocate()
private let downstream: Downstream
private let dueTime: Context.SchedulerTimeType.Stride
private let scheduler: Context
private let options: Context.SchedulerOptions?
private var state = SubscriptionStatus.awaitingSubscription
private var currentCanceller: Cancellable?
private var currentValue: Output?
private var currentGeneration: Generation = 0
private var downstreamDemand = Subscribers.Demand.none
init(downstream: Downstream,
dueTime: Context.SchedulerTimeType.Stride,
scheduler: Context,
options: Context.SchedulerOptions?) {
self.downstream = downstream
self.dueTime = dueTime
self.scheduler = scheduler
self.options = options
}
deinit {
lock.deallocate()
downstreamLock.deallocate()
}
func receive(subscription: Subscription) {
lock.lock()
guard case .awaitingSubscription = state else {
lock.unlock()
subscription.cancel()
return
}
state = .subscribed(subscription)
lock.unlock()
downstreamLock.lock()
downstream.receive(subscription: self)
downstreamLock.unlock()
subscription.request(.unlimited)
}
func receive(_ input: Input) -> Subscribers.Demand {
lock.lock()
precondition(!state.isAwaitingSubscription)
guard case .subscribed = state else {
lock.unlock()
return .none
}
currentGeneration += 1
let generation = currentGeneration
currentValue = input
let due = scheduler.now.advanced(by: dueTime)
lock.unlock()
let newCanceller = scheduler.schedule(after: due,
interval: dueTime,
tolerance: scheduler.minimumTolerance,
options: options) { [weak self] in
self?.due(generation: generation)
}
lock.lock()
let canceller = currentCanceller
currentCanceller = newCanceller
lock.unlock()
canceller?.cancel()
return .none
}
func receive(completion: Subscribers.Completion<Upstream.Failure>) {
lock.lock()
precondition(!state.isAwaitingSubscription)
guard case .subscribed = state else {
lock.unlock()
return
}
state = .terminal
let canceller = currentCanceller
lock.unlock()
canceller?.cancel()
scheduler.schedule {
self.downstreamLock.lock()
self.downstream.receive(completion: completion)
self.downstreamLock.unlock()
}
}
func request(_ demand: Subscribers.Demand) {
lock.lock()
precondition(!state.isAwaitingSubscription)
guard case .subscribed = state else {
lock.unlock()
return
}
downstreamDemand += demand
lock.unlock()
}
func cancel() {
lock.lock()
guard case .subscribed(let subscription) = state else {
lock.unlock()
return
}
state = .terminal
lock.unlock()
subscription.cancel()
}
var description: String { return "Debounce" }
var customMirror: Mirror {
let children: [Mirror.Child] = [
("downstream", downstream),
("downstreamDemand", downstreamDemand),
("currentValue", currentValue as Any)
]
return Mirror(self, children: children)
}
var playgroundDescription: Any { return description }
private func due(generation: Generation) {
lock.lock()
guard case .subscribed = state else {
lock.unlock()
return
}
// If this condition holds, it means that no values were received
// in this time frame => we should propagate the current value downstream.
guard generation == currentGeneration, let value = currentValue else {
let canceller = currentCanceller
lock.unlock()
canceller?.cancel()
return
}
let hasAnyDemand = downstreamDemand > 0
if hasAnyDemand {
downstreamDemand -= 1
}
let canceller = currentCanceller!
lock.unlock()
canceller.cancel()
guard hasAnyDemand else { return }
downstreamLock.lock()
let newDemand = downstream.receive(value)
downstreamLock.unlock()
if newDemand == .none { return }
lock.lock()
downstreamDemand += newDemand
lock.unlock()
}
}
}
@@ -24,7 +24,7 @@ extension Publisher {
/// - Parameter predicate: A closure that takes an element as a parameter and
/// returns a Boolean value that indicates whether to publish the element.
/// - Returns: A publisher that only publishes the first element of a stream
/// that satifies the predicate.
/// that satisfies the predicate.
public func first(
where predicate: @escaping (Output) -> Bool
) -> Publishers.FirstWhere<Self> {
@@ -40,7 +40,7 @@ extension Publisher {
/// - Parameter predicate: A closure that takes an element as a parameter and
/// returns a Boolean value that indicates whether to publish the element.
/// - Returns: A publisher that only publishes the first element of a stream
/// that satifies the predicate.
/// that satisfies the predicate.
public func tryFirst(
where predicate: @escaping (Output) throws -> Bool
) -> Publishers.TryFirstWhere<Self> {
@@ -6,7 +6,7 @@
extension Publisher {
/// Ingores all upstream elements, but passes along a completion
/// Ignores all upstream elements, but passes along a completion
/// state (finished or failed).
///
/// The output type of this publisher is `Never`.
@@ -59,7 +59,7 @@ extension Publishers {
/// for purposes of filtering.
public let predicate: (Output, Output) -> Bool
/// Creates a publisher that publishes only elements that dont match the previou
/// Creates a publisher that publishes only elements that dont match the previous
/// element, as evaluated by a provided closure.
///
/// - Parameter upstream: The publisher from which this publisher receives
@@ -7,7 +7,7 @@
extension Publisher {
/// Replaces nil elements in the stream with the proviced element.
/// Replaces nil elements in the stream with the provided element.
///
/// - Parameter output: The element to use when replacing `nil`.
/// - Returns: A publisher that replaces `nil` elements from
@@ -9,7 +9,7 @@ extension Publisher {
/// Returns a publisher as a class instance.
///
/// The downstream subscriber receieves elements and completion states unchanged from
/// The downstream subscriber receives elements and completion states unchanged from
/// the upstream publisher. Use this operator when you want to use
/// reference semantics, such as storing a publisher instance in a property.
///
@@ -0,0 +1,229 @@
//
// Publishers.Timeout.swift
//
//
// Created by Sergej Jaskiewicz on 14.06.2020.
//
extension Publisher {
/// Terminates publishing if the upstream publisher exceeds the specified time
/// interval without producing an element.
///
/// - Parameters:
/// - interval: The maximum time interval the publisher can go without emitting
/// an element, expressed in the time system of the scheduler.
/// - scheduler: The scheduler to deliver events on.
/// - options: Scheduler options that customize the delivery of elements.
/// - customError: A closure that executes if the publisher times out.
/// The publisher sends the failure returned by this closure to the subscriber as
/// the reason for termination.
/// - Returns: A publisher that terminates if the specified interval elapses with no
/// events received from the upstream publisher.
public func timeout<Context: Scheduler>(
_ interval: Context.SchedulerTimeType.Stride,
scheduler: Context,
options: Context.SchedulerOptions? = nil,
customError: (() -> Self.Failure)? = nil
) -> Publishers.Timeout<Self, Context> {
return .init(upstream: self,
interval: interval,
scheduler: scheduler,
options: options,
customError: customError)
}
}
extension Publishers {
public struct Timeout<Upstream: Publisher, Context: Scheduler>: Publisher {
public typealias Output = Upstream.Output
public typealias Failure = Upstream.Failure
public let upstream: Upstream
public let interval: Context.SchedulerTimeType.Stride
public let scheduler: Context
public let options: Context.SchedulerOptions?
public let customError: (() -> Upstream.Failure)?
public init(upstream: Upstream,
interval: Context.SchedulerTimeType.Stride,
scheduler: Context,
options: Context.SchedulerOptions?,
customError: (() -> Publishers.Timeout<Upstream, Context>.Failure)?) {
self.upstream = upstream
self.interval = interval
self.scheduler = scheduler
self.options = options
self.customError = customError
}
public func receive<Downsteam: Subscriber>(subscriber: Downsteam)
where Downsteam.Failure == Failure, Downsteam.Input == Output
{
let inner = Inner(downstream: subscriber,
interval: interval,
scheduler: scheduler,
options: options,
customError: customError)
upstream.subscribe(inner)
}
}
}
extension Publishers.Timeout {
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 lock = UnfairLock.allocate()
private let downstreamLock = UnfairRecursiveLock.allocate()
private let downstream: Downstream
private let interval: Context.SchedulerTimeType.Stride
private let scheduler: Context
private let options: Context.SchedulerOptions?
private let customError: (() -> Upstream.Failure)?
private var state = SubscriptionStatus.awaitingSubscription
private var didTimeout = false
private var timer: AnyCancellable?
init(downstream: Downstream,
interval: Context.SchedulerTimeType.Stride,
scheduler: Context,
options: Context.SchedulerOptions?,
customError: (() -> Upstream.Failure)?) {
self.downstream = downstream
self.interval = interval
self.scheduler = scheduler
self.options = options
self.customError = customError
}
deinit {
lock.deallocate()
downstreamLock.deallocate()
}
func receive(subscription: Subscription) {
lock.lock()
guard case .awaitingSubscription = state else {
lock.unlock()
subscription.cancel()
return
}
state = .subscribed(subscription)
timer = timeoutClock()
lock.unlock()
downstreamLock.lock()
downstream.receive(subscription: self)
downstreamLock.unlock()
subscription.request(.unlimited)
}
func receive(_ input: Upstream.Output) -> Subscribers.Demand {
lock.lock()
guard !didTimeout, case .subscribed = state else {
lock.unlock()
return .none
}
timer?.cancel()
didTimeout = false
timer = timeoutClock()
lock.unlock()
scheduler.schedule(options: options) {
self.downstreamLock.lock()
_ = self.downstream.receive(input)
self.downstreamLock.unlock()
}
return .unlimited
}
func receive(completion: Subscribers.Completion<Upstream.Failure>) {
lock.lock()
timer?.cancel()
state = .terminal
lock.unlock()
scheduler.schedule(options: options) {
self.downstreamLock.lock()
self.downstream.receive(completion: completion)
self.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 "Timeout" }
var customMirror: Mirror { return Mirror(self, children: EmptyCollection()) }
var playgroundDescription: Any { return description }
private func timedOut() {
lock.lock()
guard !didTimeout, case let .subscribed(subscription) = state else {
lock.unlock()
return
}
didTimeout = true
state = .terminal
lock.unlock()
subscription.cancel()
downstreamLock.lock()
downstream
.receive(completion: customError.map { .failure($0()) } ?? .finished)
downstreamLock.unlock()
}
private func timeoutClock() -> AnyCancellable {
let cancellable = scheduler
.schedule(after: scheduler.now.advanced(by: interval),
interval: interval,
tolerance: scheduler.minimumTolerance,
options: options,
{ [weak self] in self?.timedOut() })
return AnyCancellable { cancellable.cancel() }
}
}
}
+1 -1
View File
@@ -23,7 +23,7 @@ public protocol SchedulerTimeIntervalConvertible {
///
/// A scheduler used to execute code as soon as possible, or after a future date.
/// Individual scheduler implementations use whatever time-keeping system makes sense
/// for them. Schdedulers express this as their `SchedulerTimeType`. Since this type
/// for them. Schedulers express this as their `SchedulerTimeType`. Since this type
/// conforms to `SchedulerTimeIntervalConvertible`, you can always express these times
/// with the convenience functions like `.milliseconds(500)`. Schedulers can accept
/// options to control how they execute the actions passed to them. These options may
+1 -1
View File
@@ -27,7 +27,7 @@ public protocol Subscriber: CustomCombineIdentifierConvertible {
/// Tells the subscriber that the publisher has produced an element.
///
/// - Parameter input: The published element.
/// - Returns: A `Demand` instance indicating how many more elements the subcriber
/// - Returns: A `Demand` instance indicating how many more elements the subscriber
/// expects to receive.
func receive(_ input: Input) -> Subscribers.Demand
+1 -1
View File
@@ -7,7 +7,7 @@
/// A protocol representing the connection of a subscriber to a publisher.
///
/// Subcriptions are class constrained because a `Subscription` has identity -
/// Subscriptions are class constrained because a `Subscription` has identity -
/// defined by the moment in time a particular subscriber attached to a publisher.
/// Canceling a `Subscription` must be thread-safe.
///
@@ -166,7 +166,7 @@ extension DispatchQueue {
// as possible.
//
// By trial and error I got that the `rawValue` of `UInt64.max / 13`
// gives us propably the widest range of supported values:
// gives us probably 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.
@@ -384,7 +384,7 @@ extension DispatchQueue: OpenCombine.Scheduler {
}
#endif
// This function is taken from swift-corlibs-libdispatch:
// This function is taken from swift-corelibs-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].
@@ -44,7 +44,7 @@ extension NotificationCenter {
/// The name of notifications published by this publisher.
public let name: Notification.Name
/// The object posting the named notfication.
/// The object posting the named notification.
public let object: AnyObject?
/// Creates a publisher that emits events when broadcasting notifications.
@@ -52,7 +52,7 @@ extension NotificationCenter {
/// - 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`,
/// - object: The object posting the named notification. If `nil`,
/// the publisher emits elements for any object producing a notification
/// with the given name.
public init(center: NotificationCenter,
@@ -78,7 +78,7 @@ extension NotificationCenter {
///
/// - Parameters:
/// - name: The name of the notification to publish.
/// - object: The object posting the named notfication. If `nil`, the publisher
/// - object: The object posting the named notification. 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.
@@ -116,7 +116,7 @@ extension NotificationCenter {
///
/// - Parameters:
/// - name: The name of the notification to publish.
/// - object: The object posting the named notfication. If `nil`, the publisher
/// - object: The object posting the named notification. 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,
@@ -0,0 +1,315 @@
//
// OperationQueue+Scheduler.swift
//
//
// Created by Sergej Jaskiewicz on 14.06.2020.
//
import Foundation
import OpenCombine
extension OperationQueue {
/// A namespace for disambiguation when both OpenCombine and Combine are imported.
///
/// Foundation overlay for Combine extends `OperationQueue` with new methods and
/// nested types.
/// If you import both OpenCombine and Foundation, you will not be able
/// to write `OperationQueue.SchedulerTimeType`,
/// because Swift is unable to understand which `SchedulerTimeType`
/// you're referring to.
///
/// So you have to write `OperationQueue.OCombine.SchedulerTimeType`.
///
/// This bug is tracked [here](https://bugs.swift.org/browse/SR-11183).
///
/// You can omit this whenever Combine is not available (e. g. on Linux).
public struct OCombine: Scheduler {
public let queue: OperationQueue
public init(_ queue: OperationQueue) {
self.queue = queue
}
/// The scheduler time type used by the operation queue.
public struct SchedulerTimeType: Strideable, Codable, Hashable {
/// The date represented by this type.
public var date: Date
/// Initializes a operation queue scheduler time with the given date.
///
/// - Parameter date: The date to represent.
public init(_ date: Date) {
self.date = date
}
/// Returns the distance to another operation queue scheduler time.
///
/// - Parameter other: Another operation queue time.
/// - Returns: The time interval between this time and the provided time.
public func distance(to other: SchedulerTimeType) -> Stride {
let absoluteSelf = date.timeIntervalSinceReferenceDate
let absoluteOther = other.date.timeIntervalSinceReferenceDate
return Stride(absoluteSelf.distance(to: absoluteOther))
}
/// Returns an operation queue scheduler time calculated by advancing this
/// instances time by the given interval.
///
/// - Parameter n: A time interval to advance.
/// - Returns: An operation queue time advanced by the given interval from
/// this instances time.
public func advanced(by value: Stride) -> SchedulerTimeType {
return SchedulerTimeType(date + value.magnitude)
}
/// The interval by which operation queue times advance.
public struct Stride: SchedulerTimeIntervalConvertible,
Comparable,
SignedNumeric,
ExpressibleByFloatLiteral,
Codable {
public typealias FloatLiteralType = TimeInterval
public typealias IntegerLiteralType = TimeInterval
public typealias Magnitude = TimeInterval
/// The value of this time interval in seconds.
public var magnitude: TimeInterval
/// The value of this time interval in seconds.
public var timeInterval: TimeInterval {
return magnitude
}
public init(integerLiteral value: TimeInterval) {
magnitude = value
}
public init(floatLiteral value: TimeInterval) {
magnitude = value
}
public init(_ timeInterval: TimeInterval) {
magnitude = timeInterval
}
public init?<Source: BinaryInteger>(exactly source: Source) {
guard let value = TimeInterval(exactly: source) else { return nil }
magnitude = value
}
public static func < (lhs: Stride, rhs: Stride) -> Bool {
return lhs.magnitude < rhs.magnitude
}
public static func * (lhs: Stride, rhs: Stride) -> Stride {
return Stride(lhs.magnitude * rhs.magnitude)
}
public static func + (lhs: Stride, rhs: Stride) -> Stride {
return Stride(lhs.magnitude + rhs.magnitude)
}
public static func - (lhs: Stride, rhs: Stride) -> Stride {
return Stride(lhs.magnitude - rhs.magnitude)
}
public static func *= (lhs: inout Stride, rhs: Stride) {
lhs.magnitude *= rhs.magnitude
}
public static func += (lhs: inout Stride, rhs: Stride) {
lhs.magnitude += rhs.magnitude
}
public static func -= (lhs: inout Stride, rhs: Stride) {
lhs.magnitude -= rhs.magnitude
}
public static func seconds(_ value: Int) -> Stride {
return Stride(TimeInterval(value))
}
public static func seconds(_ value: Double) -> Stride {
return Stride(TimeInterval(value))
}
public static func milliseconds(_ value: Int) -> Stride {
return Stride(TimeInterval(value) / 1_000)
}
public static func microseconds(_ value: Int) -> Stride {
return Stride(TimeInterval(value) / 1_000_000)
}
public static func nanoseconds(_ value: Int) -> Stride {
return Stride(TimeInterval(value) / 1_000_000_000)
}
}
}
/// Options that affect the operation of the operation queue scheduler.
public struct SchedulerOptions {
}
private final class DelayReadyOperation: Operation, Cancellable {
private static let readySchedulingQueue =
DispatchQueue(label: "DelayReadyOperation")
private var action: (() -> Void)?
private var readyFromAfter = false
private let lock = UnfairLock.allocate()
init(_ action: @escaping() -> Void, after: SchedulerTimeType) {
self.action = action
super.init()
let deadline = DispatchTime.now() + after.date.timeIntervalSinceNow
DelayReadyOperation.readySchedulingQueue
.asyncAfter(deadline: deadline) { [weak self] in
self?.becomeReady()
}
}
deinit {
lock.deallocate()
}
override func main() {
action!()
action = nil
}
private func becomeReady() {
// Smart key paths don't work with NSOperation in swift-corelibs-foundation prior to
// Swift 5.1.
#if canImport(Darwin) || swift(<5.1)
// The smart key paths don't work with NSOperation on OS versions prior to
// iOS 11. The string key paths work fine everywhere.
// https://forums.swift.org/t/keypath-translation-for-kvo-notification-seems-to-not-work-properly-on-ios-10/15898
willChangeValue(forKey: "isReady")
#else
willChangeValue(for: \.isReady)
#endif
lock.lock()
readyFromAfter = true
lock.unlock()
// Smart key paths don't work with NSOperation in swift-corelibs-foundation prior to
// Swift 5.1.
#if canImport(Darwin) || swift(<5.1)
// The smart key paths don't work with NSOperation on OS versions prior to
// iOS 11. The string key paths work fine everywhere.
// https://forums.swift.org/t/keypath-translation-for-kvo-notification-seems-to-not-work-properly-on-ios-10/15898
didChangeValue(forKey: "isReady")
#else
didChangeValue(for: \.isReady)
#endif
}
override var isReady: Bool {
guard super.isReady else { return false }
lock.lock()
defer { lock.unlock() }
return readyFromAfter
}
}
public func schedule(options: SchedulerOptions?,
_ action: @escaping () -> Void) {
let op = BlockOperation(block: action)
queue.addOperation(op)
}
public func schedule(after date: SchedulerTimeType,
tolerance: SchedulerTimeType.Stride,
options: SchedulerOptions?,
_ action: @escaping () -> Void) {
let op = DelayReadyOperation(action, after: date)
queue.addOperation(op)
}
public func schedule(after date: SchedulerTimeType,
interval: SchedulerTimeType.Stride,
tolerance: SchedulerTimeType.Stride,
options: SchedulerOptions?,
_ action: @escaping () -> Void) -> Cancellable {
let op = DelayReadyOperation(action, after: date.advanced(by: interval))
queue.addOperation(op)
return AnyCancellable(op)
}
public var now: SchedulerTimeType {
return .init(Date())
}
public var minimumTolerance: SchedulerTimeType.Stride {
return .init(0.0)
}
}
/// A namespace for disambiguation when both OpenCombine and Foundation are imported.
///
/// Foundation overlay for Combine extends `OperationQueue` with new methods and
/// nested types.
/// If you import both OpenCombine and Foundation, you will not be able
/// to write `OperationQueue.main.schedule { doThings() }`,
/// because Swift is unable to understand which `schedule` method
/// you're referring to.
///
/// So you have to write `OperationQueue.main.ocombine.schedule { doThings() }`.
///
/// This bug is tracked [here](https://bugs.swift.org/browse/SR-11183).
///
/// You can omit this whenever Combine is not available (e. g. on Linux).
public var ocombine: OCombine {
return OCombine(self)
}
}
#if !canImport(Combine)
extension OperationQueue: OpenCombine.Scheduler {
/// Options that affect the operation of the run loop scheduler.
public typealias SchedulerOptions = OCombine.SchedulerOptions
/// The scheduler time type used by the run loop.
public typealias SchedulerTimeType = OCombine.SchedulerTimeType
public func schedule(options: SchedulerOptions?, _ action: @escaping () -> Void) {
ocombine.schedule(options: options, action)
}
public func schedule(after date: SchedulerTimeType,
tolerance: SchedulerTimeType.Stride,
options: SchedulerOptions?,
_ action: @escaping () -> Void) {
ocombine.schedule(after: date, tolerance: tolerance, options: options, action)
}
public func schedule(after date: SchedulerTimeType,
interval: SchedulerTimeType.Stride,
tolerance: SchedulerTimeType.Stride,
options: SchedulerOptions?,
_ action: @escaping () -> Void) -> Cancellable {
return ocombine.schedule(after: date,
interval: interval,
tolerance: tolerance,
options: options,
action)
}
public var now: SchedulerTimeType {
return ocombine.now
}
public var minimumTolerance: SchedulerTimeType.Stride {
return ocombine.minimumTolerance
}
}
#endif
@@ -8,6 +8,9 @@
import Foundation
import OpenCombine
// PropertyListEncoder and PropertyListDecoder are unavailable in
// swift-corelibs-foundation prior to Swift 5.1.
#if canImport(Darwin) || swift(>=5.1)
extension PropertyListEncoder: TopLevelEncoder {
public typealias Output = Data
}
@@ -15,3 +18,4 @@ extension PropertyListEncoder: TopLevelEncoder {
extension PropertyListDecoder: TopLevelDecoder {
public typealias Input = Data
}
#endif
@@ -1,12 +1,11 @@
//
// RunLoop.swift
// RunLoop+Scheduler.swift
//
//
// Created by Sergej Jaskiewicz on 13.12.2019.
//
import CoreFoundation
import Dispatch
import Foundation
import OpenCombine
@@ -30,8 +29,8 @@ extension RunLoop {
public let runLoop: RunLoop
public init(_ queue: RunLoop) {
self.runLoop = queue
public init(_ runLoop: RunLoop) {
self.runLoop = runLoop
}
/// The scheduler time type used by the run loop.
@@ -0,0 +1,354 @@
//
// Timer+Publisher.swift
//
//
// Created by Sergej Jaskiewicz on 23.06.2020.
//
import CoreFoundation
import Foundation
import OpenCombine
extension Timer {
/// Returns a publisher that repeatedly emits the current date on the given interval.
///
/// - Parameters:
/// - interval: The time interval on which to publish events. For example,
/// a value of `0.5` publishes an event approximately every half-second.
/// - tolerance: The allowed timing variance when emitting events.
/// Defaults to `nil`, which allows any variance.
/// - runLoop: The run loop on which the timer runs.
/// - mode: The run loop mode in which to run the timer.
/// - options: Scheduler options passed to the timer. Defaults to `nil`.
/// - Returns: A publisher that repeatedly emits the current date on the given
/// interval.
public static func publish(
every interval: TimeInterval,
tolerance _: TimeInterval? = nil,
on runLoop: RunLoop,
in mode: RunLoop.Mode,
options: RunLoop.OCombine.SchedulerOptions? = nil
) -> OCombine.TimerPublisher {
// A bug in Combine: tolerance is ignored.
return .init(interval: interval, runLoop: runLoop, mode: mode, options: options)
}
/// A namespace for disambiguation when both OpenCombine and Combine are imported.
///
/// Foundation overlay for Combine extends `Timer` with new methods and nested
/// types.
/// If you import both OpenCombine and Foundation, you will not be able
/// to write `Timer.TimerPublisher`,
/// because Swift is unable to understand which `TimerPublisher`
/// you're referring to.
///
/// So you have to write `Timer.OCombine.TimerPublisher`.
///
/// 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 enum OCombine {
/// A publisher that repeatedly emits the current date on a given interval.
public final class TimerPublisher: ConnectablePublisher {
public typealias Output = Date
public typealias Failure = Never
public let interval: TimeInterval
public let tolerance: TimeInterval?
public let runLoop: RunLoop
public let mode: RunLoop.Mode
public let options: RunLoop.OCombine.SchedulerOptions?
private lazy var routingSubscription: RoutingSubscription = {
RoutingSubscription(parent: self)
}()
/// Creates a publisher that repeatedly emits the current date
/// on the given interval.
///
/// - Parameters:
/// - interval: The interval on which to publish events.
/// - tolerance: The allowed timing variance when emitting events.
/// Defaults to `nil`, which allows any variance.
/// - runLoop: The run loop on which the timer runs.
/// - mode: The run loop mode in which to run the timer.
/// - options: Scheduler options passed to the timer. Defaults to `nil`.
public init(
interval: TimeInterval,
tolerance: TimeInterval? = nil,
runLoop: RunLoop,
mode: RunLoop.Mode,
options: RunLoop.OCombine.SchedulerOptions? = nil
) {
self.interval = interval
self.tolerance = tolerance
self.runLoop = runLoop
self.mode = mode
self.options = options
}
/// Adapter subscription to allow `Timer` to multiplex to multiple subscribers
/// the values produced by a single `TimerPublisher.Inner`
private final class RoutingSubscription
: Subscription,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
{
typealias Input = Date
typealias Failure = Never
private typealias ErasedSubscriber = AnySubscriber<Output, Failure>
private let lock = UnfairLock.allocate()
// Inner is IUP due to init requirements
// swiftlint:disable:next implicitly_unwrapped_optional
private var inner: Inner!
private var subscribers: [ErasedSubscriber] = []
private var isConnected = false
init(parent: TimerPublisher) {
inner = Inner(parent: parent, downstream: self)
}
deinit {
lock.deallocate()
}
func addSubscriber<Downstream: Subscriber>(_ downstream: Downstream)
where Downstream.Failure == Failure, Downstream.Input == Output
{
lock.lock()
subscribers.append(AnySubscriber(downstream))
lock.unlock()
downstream.receive(subscription: self)
}
func receive(_ value: Input) -> Subscribers.Demand {
var resultingDemand = Subscribers.Demand.none
lock.lock()
let subscribers = self.subscribers
let isConnected = self.isConnected
lock.unlock()
guard isConnected else {
// This branch is only reachable in case of a race condition.
return .none
}
for subscriber in subscribers {
resultingDemand += subscriber.receive(value)
}
return resultingDemand
}
func request(_ demand: Subscribers.Demand) {
lock.lock()
let inner = self.inner!
lock.unlock()
inner.request(demand)
}
func cancel() {
lock.lock()
let inner = self.inner!
isConnected = false
subscribers = []
lock.unlock()
inner.cancel()
}
var description: String { return "Timer" }
var customMirror: Mirror { return inner.customMirror }
var playgroundDescription: Any { return description }
var combineIdentifier: CombineIdentifier {
return inner.combineIdentifier
}
func startPublishing() {
lock.lock()
let isConnected = self.isConnected
self.isConnected = true
let inner = self.inner!
lock.unlock()
if isConnected { return }
inner.startPublishing()
}
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Failure == Downstream.Failure, Output == Downstream.Input
{
routingSubscription.addSubscriber(subscriber)
}
public func connect() -> Cancellable {
routingSubscription.startPublishing()
return routingSubscription
}
private typealias Parent = TimerPublisher
private final class Inner
: NSObject,
Subscription,
CustomReflectable,
CustomPlaygroundDisplayConvertible
{
private lazy var timer: CFRunLoopTimer? = {
let timer = CFRunLoopTimerCreateWithHandler(
nil,
Date().timeIntervalSinceReferenceDate,
parent?.interval ?? 0,
0,
0,
{ [weak self] _ in self?.timerFired() }
)!
CFRunLoopTimerSetTolerance(timer, parent?.tolerance ?? 0)
return timer
}()
private let lock = UnfairLock.allocate()
private var downstream: RoutingSubscription?
private var parent: Parent?
private var started = false
private var demand = Subscribers.Demand.none
init(parent: Parent, downstream: RoutingSubscription) {
self.parent = parent
self.downstream = downstream
}
deinit {
lock.deallocate()
}
func startPublishing() {
lock.lock()
guard let timer = self.timer,
let parent = self.parent,
!started else {
lock.unlock()
return
}
started = true
lock.unlock()
CFRunLoopAddTimer(parent.runLoop.getCFRunLoop(),
timer,
parent.mode.asCFRunLoopMode())
}
func cancel() {
lock.lock()
guard let timer = self.timer else {
lock.unlock()
return
}
downstream = nil
parent = nil
started = false
demand = .none
self.timer = nil
lock.unlock()
CFRunLoopTimerInvalidate(timer)
}
func request(_ demand: Subscribers.Demand) {
lock.lock()
defer { lock.unlock() }
guard parent != nil else {
return
}
self.demand += demand
}
override var description: String { return "Timer" }
var customMirror: Mirror {
lock.lock()
defer { lock.unlock() }
let children: [Mirror.Child] = [
("downstream", downstream as Any),
("interval", parent?.interval as Any),
("tolerance", parent?.tolerance as Any),
]
return Mirror(self, children: children)
}
var playgroundDescription: Any { return description }
private func timerFired() {
lock.lock()
guard let downstream = self.downstream,
parent != nil,
demand > 0
else {
lock.unlock()
return
}
demand -= 1
lock.unlock()
let newDemand = downstream.receive(Date())
guard newDemand > 0 else {
return
}
lock.lock()
demand += newDemand
lock.unlock()
}
}
}
}
}
#if !canImport(Combine)
extension Timer {
/// A publisher that repeatedly emits the current date on a given interval.
public typealias TimerPublisher = OCombine.TimerPublisher
}
#endif
extension RunLoop.Mode {
fileprivate func asCFRunLoopMode() -> CFRunLoopMode {
#if canImport(Darwin)
return CFRunLoopMode(rawValue as CFString)
#else
return rawValue.withCString {
#if swift(>=5.3)
let encoding = CFStringBuiltInEncodings.UTF8.rawValue
#else
let encoding = CFStringEncoding(kCFStringEncodingUTF8)
#endif // swift(>=5.3)
return CFStringCreateWithCString(
nil,
$0,
encoding
)
}
#endif
}
}
@@ -102,6 +102,36 @@ final class CurrentValueSubjectTests: XCTestCase {
XCTAssertEqual(numberOfInputsHistory, expectedNumberOfInputsHistory)
}
func testRequestSeveralTimes() throws {
let cvs = Sut(-1)
var downstreamSubscription: Subscription?
let tracking = TrackingSubscriber(
receiveSubscription: { downstreamSubscription = $0 }
)
cvs.subscribe(tracking)
XCTAssertEqual(tracking.history, [.subscription("CurrentValueSubject")])
try XCTUnwrap(downstreamSubscription).request(.max(2))
try XCTUnwrap(downstreamSubscription).request(.max(3))
try XCTUnwrap(downstreamSubscription).request(.max(1))
XCTAssertEqual(tracking.history, [.subscription("CurrentValueSubject"),
.value(-1)])
for i in 0 ..< 10 {
cvs.send(i)
}
XCTAssertEqual(tracking.history, [.subscription("CurrentValueSubject"),
.value(-1),
.value(0),
.value(1),
.value(2),
.value(3),
.value(4)])
}
func testCrashOnZeroInitialDemand() {
assertCrashes {
let subscriber = TrackingSubscriber(
@@ -139,6 +169,20 @@ final class CurrentValueSubjectTests: XCTestCase {
.completion(.failure(.oops))])
}
func testChangeValueAfterCompletion() {
let cvs = Sut(0)
cvs.send(completion: .finished)
cvs.value = 42
XCTAssertEqual(cvs.value, 42)
}
func testSendValueAfterCompletion() {
let cvs = Sut(0)
cvs.send(completion: .finished)
cvs.send(42)
XCTAssertEqual(cvs.value, 0)
}
func testMultipleSubscriptions() {
let cvs = Sut(112)
@@ -224,6 +268,42 @@ final class CurrentValueSubjectTests: XCTestCase {
.subscription("CurrentValueSubject"),
.value(112),
.subscription("CurrentValueSubject")])
for (i, subscription) in subscriber.tracking.subscriptions.enumerated()
where i.isMultiple(of: 2)
{
subscription.cancel()
}
cvs.value = 200
XCTAssertEqual(subscriber.tracking.history,
[.value(112),
.subscription("CurrentValueSubject"),
.value(112),
.subscription("CurrentValueSubject"),
.value(112),
.subscription("CurrentValueSubject"),
.value(112),
.subscription("CurrentValueSubject"),
.value(112),
.subscription("CurrentValueSubject"),
.value(112),
.subscription("CurrentValueSubject"),
.value(112),
.subscription("CurrentValueSubject"),
.value(112),
.subscription("CurrentValueSubject"),
.value(112),
.subscription("CurrentValueSubject"),
.value(112),
.subscription("CurrentValueSubject"),
.value(112),
.subscription("CurrentValueSubject"),
.value(200),
.value(200),
.value(200),
.value(200),
.value(200)])
}
// Reactive Streams Spec: Rule #6
@@ -375,6 +455,100 @@ final class CurrentValueSubjectTests: XCTestCase {
XCTAssertEqual(subscription2.history, [.requested(.unlimited)])
}
func testCompletion() throws {
let passthrough = Sut(42)
var downstreamSubscription: Subscription?
let tracking = TrackingSubscriber(
receiveSubscription: { downstreamSubscription = $0 }
)
passthrough.subscribe(tracking)
try XCTUnwrap(downstreamSubscription).request(.max(12))
passthrough.send(1)
expectedChildren(
("parent", .contains("CurrentValueSubject")),
("downstream", .contains("TrackingSubscriberBase")),
("demand", "max(10)"),
("subject", .contains("CurrentValueSubject"))
)(Mirror(reflecting: try XCTUnwrap(downstreamSubscription)))
passthrough.send(completion: .finished)
if !hasCustomMirrorUseAfterFreeBug {
expectedChildren(
("parent", "nil"),
("downstream", "nil"),
("demand", "max(10)"),
("subject", "nil")
)(Mirror(reflecting: try XCTUnwrap(downstreamSubscription)))
}
XCTAssertEqual(tracking.history, [.subscription("CurrentValueSubject"),
.value(42),
.value(1),
.completion(.finished)])
passthrough.send(completion: .failure(.oops))
try XCTUnwrap(downstreamSubscription).cancel()
try XCTUnwrap(downstreamSubscription).request(.max(3))
if !hasCustomMirrorUseAfterFreeBug {
expectedChildren(
("parent", "nil"),
("downstream", "nil"),
("demand", "max(10)"),
("subject", "nil")
)(Mirror(reflecting: try XCTUnwrap(downstreamSubscription)))
}
XCTAssertEqual(tracking.history, [.subscription("CurrentValueSubject"),
.value(42),
.value(1),
.completion(.finished)])
}
func testCancellation() throws {
let cvs = Sut(42)
var downstreamSubscription: Subscription?
let tracking = TrackingSubscriber(
receiveSubscription: { downstreamSubscription = $0 }
)
cvs.subscribe(tracking)
try XCTUnwrap(downstreamSubscription).request(.max(12))
cvs.send(1)
expectedChildren(
("parent", .contains("CurrentValueSubject")),
("downstream", .contains("TrackingSubscriberBase")),
("demand", "max(10)"),
("subject", .contains("CurrentValueSubject"))
)(Mirror(reflecting: try XCTUnwrap(downstreamSubscription)))
try XCTUnwrap(downstreamSubscription).cancel()
try XCTUnwrap(downstreamSubscription).cancel()
try XCTUnwrap(downstreamSubscription).request(.max(3))
try XCTUnwrap(downstreamSubscription).request(.max(4))
if !hasCustomMirrorUseAfterFreeBug {
expectedChildren(
("parent", "nil"),
("downstream", "nil"),
("demand", "max(10)"),
("subject", "nil")
)(Mirror(reflecting: try XCTUnwrap(downstreamSubscription)))
}
XCTAssertEqual(tracking.history, [.subscription("CurrentValueSubject"),
.value(42),
.value(1)])
}
func testLifecycle() throws {
var deinitCounter = 0
@@ -425,68 +599,87 @@ final class CurrentValueSubjectTests: XCTestCase {
XCTAssertEqual(deinitCounter, 2)
}
func testSynchronization() {
let subscriptions = Atomic<[Subscription]>([])
let inputs = Atomic<[Int]>([])
let completions = Atomic<[Subscribers.Completion<TestingError>]>([])
let cvs = Sut(112)
let subscriber = AnySubscriber<Int, TestingError>(
receiveSubscription: { subscription in
subscriptions.do { $0.append(subscription) }
subscription.request(.unlimited)
},
receiveValue: { value in
inputs.do { $0.append(value) }
return .none
},
receiveCompletion: { completion in
completions.do { $0.append(completion) }
func testCancelsUpstreamSubscriptionsOnDeinit() {
let subscription = CustomSubscription()
do {
let cvs = Sut(42)
for _ in 0 ..< 5 {
cvs.send(subscription: subscription)
}
)
XCTAssertEqual(subscription.history, [.requested(.unlimited),
.requested(.unlimited),
.requested(.unlimited),
.requested(.unlimited),
.requested(.unlimited)])
}
race(
{
cvs.subscribe(subscriber)
},
{
cvs.subscribe(subscriber)
XCTAssertEqual(subscription.history, [.requested(.unlimited),
.requested(.unlimited),
.requested(.unlimited),
.requested(.unlimited),
.requested(.unlimited),
.cancelled,
.cancelled,
.cancelled,
.cancelled,
.cancelled])
}
func testReleasesEverythingOnTermination() {
enum TerminationReason: CaseIterable {
case cancelled
case finished
case failed
}
for reason in TerminationReason.allCases {
weak var weakSubscriber: TrackingSubscriber?
weak var weakSubject: Sut?
weak var weakSubscription: AnyObject?
do {
let subject = Sut(42)
do {
let subscriber = TrackingSubscriber(
receiveSubscription: {
weakSubscription = $0 as AnyObject
}
)
weakSubscriber = subscriber
weakSubject = subject
subject.subscribe(subscriber)
}
switch reason {
case .cancelled:
(weakSubscription as? Subscription)?.cancel()
case .finished:
subject.send(completion: .finished)
case .failed:
subject.send(completion: .failure(.oops))
}
XCTAssertNil(weakSubscriber, "Subscriber leaked - \(reason)")
XCTAssertNil(weakSubscription, "Subscription leaked - \(reason)")
}
XCTAssertNil(weakSubject, "Subject leaked - \(reason)")
}
}
func testConduitReflection() throws {
try testSubscriptionReflection(
description: "CurrentValueSubject",
customMirror: expectedChildren(
("parent", .contains("CurrentValueSubject")),
("downstream", .contains("TrackingSubscriberBase")),
("demand", "max(0)"),
("subject", .contains("CurrentValueSubject"))
),
playgroundDescription: "CurrentValueSubject",
sut: CurrentValueSubject<Int, Error>(42)
)
XCTAssertEqual(subscriptions.value.count, 200)
race(
{
cvs.value = 42
},
{
cvs.value = 42
}
)
XCTAssertEqual(inputs.value.count, 40200)
XCTAssertEqual(cvs.value, 42)
race(
{
subscriptions.value[0].request(.max(4))
},
{
subscriptions.value[0].request(.max(10))
}
)
race(
{
cvs.send(completion: .finished)
},
{
cvs.send(completion: .failure(""))
}
)
XCTAssertEqual(completions.value.count, 200)
}
}
@@ -0,0 +1,453 @@
//
// OperationQueueSchedulerTests.swift
//
//
// Created by Sergej Jaskiewicz on 14.06.2020.
//
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 OperationQueueSchedulerTests: XCTestCase {
// MARK: - Scheduler.SchedulerTimeType
func testSchedulerTimeTypeDistance() {
RunLoopSchedulerTests.testSchedulerTimeTypeDistance(OperationQueueScheduler.self)
}
func testSchedulerTimeTypeAdvanced() {
RunLoopSchedulerTests.testSchedulerTimeTypeAdvanced(OperationQueueScheduler.self)
}
func testSchedulerTimeTypeEquatable() {
RunLoopSchedulerTests.testSchedulerTimeTypeEquatable(OperationQueueScheduler.self)
}
func testSchedulerTimeTypeCodable() throws {
try RunLoopSchedulerTests
.testSchedulerTimeTypeCodable(OperationQueueScheduler.self)
}
// MARK: - Scheduler.SchedulerTimeType.Stride
func testStrideToTimeInterval() {
RunLoopSchedulerTests.testStrideToTimeInterval(OperationQueueScheduler.self)
}
func testStrideFromTimeInterval() {
RunLoopSchedulerTests.testStrideFromTimeInterval(OperationQueueScheduler.self)
}
func testStrideFromNumericValue() {
RunLoopSchedulerTests.testStrideFromNumericValue(OperationQueueScheduler.self)
}
func testStrideComparable() {
RunLoopSchedulerTests.testStrideComparable(OperationQueueScheduler.self)
}
func testStrideMultiplication() {
RunLoopSchedulerTests.testStrideMultiplication(OperationQueueScheduler.self)
}
func testStrideAddition() {
RunLoopSchedulerTests.testStrideAddition(OperationQueueScheduler.self)
}
func testStrideSubtraction() {
RunLoopSchedulerTests.testStrideSubtraction(OperationQueueScheduler.self)
}
func testStrideCodable() throws {
try RunLoopSchedulerTests.testStrideCodable(OperationQueueScheduler.self)
}
// MARK: - Scheduler
#if canImport(Darwin)
// FIXME: These tests crash with swift-corelibs-foundation.
// The issue has been resolved in
// https://github.com/apple/swift-corelibs-foundation/pull/2779
// but it hasn't made it into an official release yet.
func testScheduleActionOnceNowWithTestQueue() {
let queue = TestOperationQueue()
let scheduler = makeScheduler(queue)
let counter = Atomic(0)
scheduler.schedule {
counter.do { $0 += 1 }
}
XCTAssertEqual(queue.history.count, 1)
guard case let .addOperation(op as BlockOperation)? = queue.history.first else {
XCTFail("Unexpected history")
return
}
queue.waitUntilAllOperationsAreFinished()
XCTAssertEqual(counter.value, 1)
op.main()
XCTAssertEqual(counter.value, 2)
op.main()
XCTAssertEqual(counter.value, 3)
}
func testScheduleActionOnceNowWithRealQueue() {
let mainQueue = OperationQueue.main
let now = Date()
var actualDate = Date.distantPast
executeOnBackgroundThread {
makeScheduler(mainQueue).schedule {
XCTAssertTrue(Thread.isMainThread)
actualDate = Date()
XCTAssertNotNil(OperationQueue.current)
RunLoop.current.run(until: Date() + 0.01)
}
}
XCTAssertEqual(actualDate, .distantPast)
RunLoop.main.run(until: Date() + 0.05)
XCTAssertEqual(actualDate.timeIntervalSinceReferenceDate,
now.timeIntervalSinceReferenceDate,
accuracy: 0.1)
}
func testScheduleActionOnceLaterWithTestQueue() {
let queue = TestOperationQueue()
let scheduler = makeScheduler(queue)
let desiredDelay: TimeInterval = 0.6
let counter = Atomic(0)
scheduler.schedule(after: scheduler.now.advanced(by: .init(desiredDelay))) {
counter.do { $0 += 1 }
}
XCTAssertEqual(queue.history.count, 1)
guard case let .addOperation(op)? = queue.history.first else {
XCTFail("Unexpected history")
return
}
XCTAssertFalse(op is BlockOperation)
XCTAssertFalse(op.isReady)
XCTAssertFalse(op.isFinished)
XCTAssertFalse(op.isCancelled)
XCTAssertFalse(op.isAsynchronous)
XCTAssertFalse(op.isConcurrent)
XCTAssert(op is Cancellable)
XCTAssertEqual(counter.value, 0)
let now = Date()
queue.waitUntilAllOperationsAreFinished()
XCTAssertEqual(counter.value, 1)
XCTAssertEqual(Date().timeIntervalSinceReferenceDate,
(now + desiredDelay).timeIntervalSinceReferenceDate,
accuracy: desiredDelay / 3)
assertCrashes {
op.main()
}
}
func testScheduleActionOnceLaterWithRealQueue() {
let mainQueue = OperationQueue.main
let startDate = Date()
var actualDate = Date.distantPast
let desiredDelay: TimeInterval = 2
executeOnBackgroundThread {
let scheduler = makeScheduler(mainQueue)
scheduler
.schedule(after: scheduler.now.advanced(by: .init(desiredDelay))) {
XCTAssertTrue(Thread.isMainThread)
actualDate = Date()
XCTAssertNotNil(OperationQueue.current)
}
}
XCTAssertEqual(actualDate, .distantPast)
RunLoop.main.run(until: Date() + desiredDelay * 2)
XCTAssertEqual(
actualDate.timeIntervalSinceReferenceDate -
startDate.timeIntervalSinceReferenceDate,
desiredDelay,
accuracy: desiredDelay / 3
)
}
func testScheduleRepeatingWithTestQueue() {
let queue = TestOperationQueue()
let scheduler = makeScheduler(queue)
let desiredDelay: TimeInterval = 0.7
let desiredInterval: TimeInterval = 0.3
let counter = Atomic(0)
let cancellable = scheduler
.schedule(after: scheduler.now.advanced(by: .init(desiredDelay)),
interval: .init(desiredInterval)) {
counter.do { $0 += 1 }
}
XCTAssertEqual(queue.history.count, 1)
guard case let .addOperation(op)? = queue.history.first else {
XCTFail("Unexpected history")
return
}
XCTAssertFalse(op is BlockOperation)
XCTAssertFalse(op.isReady)
XCTAssertFalse(op.isFinished)
XCTAssertFalse(op.isCancelled)
XCTAssertFalse(op.isAsynchronous)
XCTAssertFalse(op.isConcurrent)
XCTAssert(op is Cancellable)
XCTAssert(cancellable is AnyCancellable)
XCTAssertEqual(counter.value, 0)
let now = Date()
queue.waitUntilAllOperationsAreFinished()
XCTAssertEqual(counter.value, 1)
let expectedDelay = desiredDelay + desiredInterval
XCTAssertEqual(Date().timeIntervalSinceReferenceDate,
(now + expectedDelay).timeIntervalSinceReferenceDate,
accuracy: expectedDelay / 3)
assertCrashes {
op.main()
}
}
func testScheduleRepeatingWithRealQueue() {
let mainQueue = OperationQueue.main
let startDate = Date()
let desiredDelay: TimeInterval = 0.7
let desiredInterval: TimeInterval = 0.3
let ticks = Atomic([TimeInterval]())
let cancellable = executeOnBackgroundThread { () -> Cancellable in
let scheduler = makeScheduler(mainQueue)
return scheduler
.schedule(after: scheduler.now.advanced(by: .init(desiredDelay)),
interval: .init(desiredInterval)) {
XCTAssertTrue(Thread.isMainThread)
ticks.do { $0.append(Date().timeIntervalSinceReferenceDate) }
XCTAssertNotNil(OperationQueue.current)
}
}
XCTAssert(cancellable is AnyCancellable)
XCTAssertEqual(ticks.value.count, 0)
RunLoop.main.run(until: Date() + 0.001)
XCTAssertEqual(ticks.value.count, 0)
// The OperationQueue scheduler doesn't repeat actions.
// Wait some extra time to make sure this is the case.
RunLoop.main.run(until: Date() + desiredDelay + desiredInterval * 5)
if ticks.value.isEmpty {
XCTFail("The scheduler doesn't work")
return
}
XCTAssertEqual(ticks.value.count, 1)
let expectedDelay = desiredDelay + desiredInterval
XCTAssertEqual(
ticks.value[0],
(startDate + expectedDelay).timeIntervalSinceReferenceDate,
accuracy: expectedDelay / 3
)
}
#endif // canImport(Darwin)
func testMinimumTolerance() {
let scheduler = makeScheduler(.main)
XCTAssertEqual(scheduler.minimumTolerance, .init(0))
}
func testNow() {
let scheduler = makeScheduler(.main)
XCTAssertEqual(scheduler.now.date.timeIntervalSinceReferenceDate,
Date().timeIntervalSinceReferenceDate,
accuracy: 0.001)
}
}
#if OPENCOMBINE_COMPATIBILITY_TEST || !canImport(Combine)
private typealias OperationQueueScheduler = OperationQueue
private func makeScheduler(_ queue: OperationQueue) -> OperationQueueScheduler {
return queue
}
#else
private typealias OperationQueueScheduler = OperationQueue.OCombine
private func makeScheduler(_ queue: OperationQueue) -> OperationQueueScheduler {
return queue.ocombine
}
#endif
@available(macOS 10.15, iOS 13.0, *)
extension OperationQueueScheduler.SchedulerTimeType.Stride
: TimeIntervalBackedSchedulerStride
{}
@available(macOS 10.15, iOS 13.0, *)
extension OperationQueueScheduler.SchedulerTimeType: DateBackedSchedulerTimeType {}
extension OperationQueueScheduler: RunLoopLikeScheduler {}
private final class TestOperationQueue: OperationQueue {
enum Event {
case progress
case addOperation(Operation)
case addOperations([Operation], waitUntilFinished: Bool)
case addBlockOperation(() -> Void)
case addBarrierBlock(() -> Void)
case getMaxConcurrentOperationCount
case setMaxConcurrentOperationCount(Int)
case getIsSuspended
case setIsSuspended(Bool)
case getName
case setName(String?)
case getQualityOfService
case setQualityOfService(QualityOfService)
case getUnderlyingQueue
case setUnderlyingQueue(DispatchQueue?)
case cancelAllOperations
case waitUntilAllOperationsAreFinished
case operations
case operationCount
}
private(set) var history = [Event]()
#if swift(>=5.1)
@available(macOS 10.15, iOS 13.0, *)
override var progress: Progress {
history.append(.progress)
return super.progress
}
#endif // swift(>=5.1)
override func addOperation(_ op: Operation) {
history.append(.addOperation(op))
super.addOperation(op)
}
override func addOperations(_ ops: [Operation], waitUntilFinished wait: Bool) {
history.append(.addOperations(ops, waitUntilFinished: wait))
super.addOperations(ops, waitUntilFinished: wait)
}
override func addOperation(_ block: @escaping () -> Void) {
history.append(.addBlockOperation(block))
super.addOperation(block)
}
#if swift(>=5.1)
@available(macOS 10.15, iOS 13.0, *)
override func addBarrierBlock(_ barrier: @escaping () -> Void) {
history.append(.addBarrierBlock(barrier))
super.addBarrierBlock(barrier)
}
#endif // swift(>=5.1)
override var maxConcurrentOperationCount: Int {
get {
history.append(.getMaxConcurrentOperationCount)
return super.maxConcurrentOperationCount
}
set {
history.append(.setMaxConcurrentOperationCount(newValue))
super.maxConcurrentOperationCount = newValue
}
}
override var isSuspended: Bool {
get {
history.append(.getIsSuspended)
return super.isSuspended
}
set {
history.append(.setIsSuspended(newValue))
super.isSuspended = newValue
}
}
override var name: String? {
get {
history.append(.getName)
return super.name
}
set {
history.append(.setName(newValue))
super.name = newValue
}
}
override var qualityOfService: QualityOfService {
get {
history.append(.getQualityOfService)
return super.qualityOfService
}
set {
history.append(.setQualityOfService(newValue))
super.qualityOfService = newValue
}
}
override var underlyingQueue: DispatchQueue? {
get {
history.append(.getUnderlyingQueue)
return super.underlyingQueue
}
set {
history.append(.setUnderlyingQueue(newValue))
super.underlyingQueue = newValue
}
}
override func cancelAllOperations() {
history.append(.cancelAllOperations)
super.cancelAllOperations()
}
override func waitUntilAllOperationsAreFinished() {
history.append(.waitUntilAllOperationsAreFinished)
super.waitUntilAllOperationsAreFinished()
}
// These properties are declared in an extension in swift-corelibs-foundation,
// so they can't be overridden.
#if canImport(Darwin)
override var operations: [Operation] {
history.append(.operations)
return super.operations
}
override var operationCount: Int {
history.append(.operationCount)
return super.operationCount
}
#endif // canImport(Darwin)
}
@@ -21,10 +21,16 @@ final class RunLoopSchedulerTests: XCTestCase {
// MARK: - Scheduler.SchedulerTimeType
func testSchedulerTimeTypeDistance() {
let time1 = Scheduler.SchedulerTimeType(Date(timeIntervalSince1970: 10_000))
let time2 = Scheduler.SchedulerTimeType(Date(timeIntervalSince1970: 10_431))
let distantFuture = Scheduler.SchedulerTimeType(.distantFuture)
let notSoDistantFuture = Scheduler.SchedulerTimeType(
RunLoopSchedulerTests.testSchedulerTimeTypeDistance(RunLoopScheduler.self)
}
static func testSchedulerTimeTypeDistance<Context: RunLoopLikeScheduler>(
_ schedulerType: Context.Type
) {
let time1 = Context.SchedulerTimeType(Date(timeIntervalSince1970: 10_000))
let time2 = Context.SchedulerTimeType(Date(timeIntervalSince1970: 10_431))
let distantFuture = Context.SchedulerTimeType(.distantFuture)
let notSoDistantFuture = Context.SchedulerTimeType(
Date.distantFuture - 1024
)
@@ -52,12 +58,18 @@ final class RunLoopSchedulerTests: XCTestCase {
}
func testSchedulerTimeTypeAdvanced() {
RunLoopSchedulerTests.testSchedulerTimeTypeAdvanced(RunLoopScheduler.self)
}
static func testSchedulerTimeTypeAdvanced<Context: RunLoopLikeScheduler>(
_ schedulerType: Context.Type
) {
let time =
Scheduler.SchedulerTimeType(Date(timeIntervalSinceReferenceDate: 10_000))
Context.SchedulerTimeType(Date(timeIntervalSinceReferenceDate: 10_000))
let beginningOfTime =
Scheduler.SchedulerTimeType(Date(timeIntervalSinceReferenceDate: 1))
let stride1 = Scheduler.SchedulerTimeType.Stride.seconds(431)
let stride2 = Scheduler.SchedulerTimeType.Stride.seconds(-220)
Context.SchedulerTimeType(Date(timeIntervalSinceReferenceDate: 1))
let stride1 = Context.SchedulerTimeType.Stride.seconds(431)
let stride2 = Context.SchedulerTimeType.Stride.seconds(-220)
XCTAssertEqual(time.advanced(by: stride1),
.init(Date(timeIntervalSinceReferenceDate: 10431)))
@@ -91,12 +103,15 @@ final class RunLoopSchedulerTests: XCTestCase {
}
func testSchedulerTimeTypeEquatable() {
let time1 =
Scheduler.SchedulerTimeType(Date(timeIntervalSinceReferenceDate: 10000))
let time2 =
Scheduler.SchedulerTimeType(Date(timeIntervalSinceReferenceDate: 10000))
let time3 =
Scheduler.SchedulerTimeType(Date(timeIntervalSinceReferenceDate: 10001))
RunLoopSchedulerTests.testSchedulerTimeTypeEquatable(RunLoopScheduler.self)
}
static func testSchedulerTimeTypeEquatable<Context: RunLoopLikeScheduler>(
_ schedulerType: Context.Type
) {
let time1 = Context.SchedulerTimeType(Date(timeIntervalSinceReferenceDate: 10000))
let time2 = Context.SchedulerTimeType(Date(timeIntervalSinceReferenceDate: 10000))
let time3 = Context.SchedulerTimeType(Date(timeIntervalSinceReferenceDate: 10001))
XCTAssertEqual(time1, time1)
XCTAssertEqual(time2, time2)
@@ -109,11 +124,17 @@ final class RunLoopSchedulerTests: XCTestCase {
}
func testSchedulerTimeTypeCodable() throws {
try RunLoopSchedulerTests.testSchedulerTimeTypeCodable(RunLoopScheduler.self)
}
static func testSchedulerTimeTypeCodable<Context: RunLoopLikeScheduler>(
_ schedulerType: Context.Type
) throws {
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let time =
Scheduler.SchedulerTimeType(Date(timeIntervalSinceReferenceDate: 1024.75))
Context.SchedulerTimeType(Date(timeIntervalSinceReferenceDate: 1024.75))
let encodedData = try encoder
.encode(time)
let encodedString = String(decoding: encodedData, as: UTF8.self)
@@ -122,7 +143,7 @@ final class RunLoopSchedulerTests: XCTestCase {
#"{"date":1024.75}"#)
let decodedTime = try decoder
.decode(Scheduler.SchedulerTimeType.self, from: encodedData)
.decode(Context.SchedulerTimeType.self, from: encodedData)
XCTAssertEqual(decodedTime, time)
}
@@ -130,6 +151,13 @@ final class RunLoopSchedulerTests: XCTestCase {
// MARK: - Scheduler.SchedulerTimeType.Stride
func testStrideToTimeInterval() {
RunLoopSchedulerTests.testStrideToTimeInterval(RunLoopScheduler.self)
}
static func testStrideToTimeInterval<Context: RunLoopLikeScheduler>(
_ schedulerType: Context.Type
) {
typealias Stride = Context.SchedulerTimeType.Stride
XCTAssertEqual(Stride.seconds(2).timeInterval, 2)
XCTAssertEqual(Stride.seconds(2.2).timeInterval, 2.2)
XCTAssertEqual(Stride.seconds(Double.infinity).timeInterval, .infinity)
@@ -153,7 +181,14 @@ final class RunLoopSchedulerTests: XCTestCase {
#endif
}
func testStrideFromTimeInterval() throws {
func testStrideFromTimeInterval() {
RunLoopSchedulerTests.testStrideFromTimeInterval(RunLoopScheduler.self)
}
static func testStrideFromTimeInterval<Context: RunLoopLikeScheduler>(
_ schedulerType: Context.Type
) {
typealias Stride = Context.SchedulerTimeType.Stride
XCTAssertEqual(Stride(2).magnitude, 2)
XCTAssertEqual(Stride(2.2).magnitude, 2.2)
XCTAssertEqual(Stride(.infinity).magnitude, .infinity)
@@ -164,6 +199,14 @@ final class RunLoopSchedulerTests: XCTestCase {
}
func testStrideFromNumericValue() {
RunLoopSchedulerTests.testStrideFromNumericValue(RunLoopScheduler.self)
}
static func testStrideFromNumericValue<Context: RunLoopLikeScheduler>(
_ schedulerType: Context.Type
) {
typealias Stride = Context.SchedulerTimeType.Stride
XCTAssertEqual((1.2 as Stride).magnitude, 1.2)
XCTAssertEqual((2 as Stride).magnitude, 2)
@@ -172,12 +215,27 @@ final class RunLoopSchedulerTests: XCTestCase {
}
func testStrideComparable() {
RunLoopSchedulerTests.testStrideComparable(RunLoopScheduler.self)
}
static func testStrideComparable<Context: RunLoopLikeScheduler>(
_ schedulerType: Context.Type
) {
typealias Stride = Context.SchedulerTimeType.Stride
XCTAssertLessThan(Stride.nanoseconds(1), .nanoseconds(2))
XCTAssertGreaterThan(Stride.nanoseconds(-2), .microseconds(-10))
XCTAssertLessThan(Stride.milliseconds(2), .seconds(2))
}
func testStrideMultiplication() {
RunLoopSchedulerTests.testStrideMultiplication(RunLoopScheduler.self)
}
static func testStrideMultiplication<Context: RunLoopLikeScheduler>(
_ schedulerType: Context.Type
) {
typealias Stride = Context.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, 1.8E-17)
@@ -237,6 +295,14 @@ final class RunLoopSchedulerTests: XCTestCase {
}
func testStrideAddition() {
RunLoopSchedulerTests.testStrideAddition(RunLoopScheduler.self)
}
static func testStrideAddition<Context: RunLoopLikeScheduler>(
_ schedulerType: Context.Type
) {
typealias Stride = Context.SchedulerTimeType.Stride
XCTAssertEqual((Stride.nanoseconds(0) + .microseconds(2)).magnitude, 2E-06)
XCTAssertEqual((Stride.nanoseconds(2) + .microseconds(0)).magnitude, 2E-09)
XCTAssertEqual((Stride.nanoseconds(7) + .nanoseconds(12)).magnitude,
@@ -298,6 +364,14 @@ final class RunLoopSchedulerTests: XCTestCase {
}
func testStrideSubtraction() {
RunLoopSchedulerTests.testStrideSubtraction(RunLoopScheduler.self)
}
static func testStrideSubtraction<Context: RunLoopLikeScheduler>(
_ schedulerType: Context.Type
) {
typealias Stride = Context.SchedulerTimeType.Stride
XCTAssertEqual((Stride.nanoseconds(0) - .microseconds(2)).magnitude, -2E-06)
XCTAssertEqual((Stride.nanoseconds(2) - .microseconds(0)).magnitude, 2E-09)
XCTAssertEqual((Stride.nanoseconds(7) - .nanoseconds(12)).magnitude, -5E-09)
@@ -359,6 +433,14 @@ final class RunLoopSchedulerTests: XCTestCase {
}
func testStrideCodable() throws {
try RunLoopSchedulerTests.testStrideCodable(RunLoopScheduler.self)
}
static func testStrideCodable<Context: RunLoopLikeScheduler>(
_ schedulerType: Context.Type
) throws {
typealias Stride = Context.SchedulerTimeType.Stride
let encoder = JSONEncoder()
let decoder = JSONDecoder()
@@ -487,25 +569,65 @@ final class RunLoopSchedulerTests: XCTestCase {
XCTAssertEqual(numberOfTicksRightAfterCancellation,
numberOfTicksOneSecondAfterCancellation)
}
func testMinimumTolerance() {
let scheduler = makeScheduler(.main)
XCTAssertEqual(scheduler.minimumTolerance, .init(0))
}
func testNow() {
let scheduler = makeScheduler(.main)
XCTAssertEqual(scheduler.now.date.timeIntervalSinceReferenceDate,
Date().timeIntervalSinceReferenceDate,
accuracy: 0.001)
}
}
#if OPENCOMBINE_COMPATIBILITY_TEST || !canImport(Combine)
private typealias Scheduler = RunLoop
private typealias RunLoopScheduler = RunLoop
private func makeScheduler(_ runLoop: RunLoop) -> RunLoop {
private func makeScheduler(_ runLoop: RunLoop) -> RunLoopScheduler {
return runLoop
}
#else
private typealias Scheduler = RunLoop.OCombine
private typealias RunLoopScheduler = RunLoop.OCombine
private func makeScheduler(_ runLoop: RunLoop) -> RunLoop.OCombine {
private func makeScheduler(_ runLoop: RunLoop) -> RunLoopScheduler {
return runLoop.ocombine
}
#endif
protocol DateBackedSchedulerTimeType: Strideable, Codable, Hashable {
init(_ date: Date)
var date: Date { get }
}
protocol TimeIntervalBackedSchedulerStride: SchedulerTimeIntervalConvertible,
Comparable,
SignedNumeric,
ExpressibleByFloatLiteral,
Codable
where Magnitude == TimeInterval
{
init(_ timeInterval: TimeInterval)
var timeInterval: TimeInterval { get }
}
protocol RunLoopLikeScheduler: Scheduler
where SchedulerTimeType: DateBackedSchedulerTimeType,
SchedulerTimeType.Stride: TimeIntervalBackedSchedulerStride {
}
@available(macOS 10.15, iOS 13.0, *)
private typealias Stride = Scheduler.SchedulerTimeType.Stride
extension RunLoopScheduler.SchedulerTimeType.Stride: TimeIntervalBackedSchedulerStride {}
@available(macOS 10.15, iOS 13.0, *)
extension RunLoopScheduler.SchedulerTimeType: DateBackedSchedulerTimeType {}
extension RunLoopScheduler: RunLoopLikeScheduler {}
@@ -0,0 +1,224 @@
//
// TimerPublisherTests.swift
//
//
// Created by Sergej Jaskiewicz on 23.06.2020.
//
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 TimerPublisherTests: XCTestCase {
func testPublishMethod() {
let publisher: TimerPublisher = Timer
.publish(every: 0.25,
tolerance: 0.02,
on: .main,
in: RunLoop.Mode(rawValue: "testMode"),
options: nil)
XCTAssertEqual(publisher.interval, 0.25)
XCTAssertEqual(publisher.tolerance, nil)
XCTAssertEqual(publisher.runLoop, .main)
XCTAssertEqual(publisher.mode, RunLoop.Mode(rawValue: "testMode"))
XCTAssertNil(publisher.options)
}
func testConnectAndPublish() {
let desiredInterval: TimeInterval = 0.5
var ticks = [TimeInterval]()
let tracking1 = TrackingSubscriberBase<Date, Never>(
receiveSubscription: {
$0.request(.max(3))
},
receiveValue: {
ticks.append($0.timeIntervalSinceReferenceDate)
return ticks.count < 3 ? .max(1) : .none
}
)
let tracking2 = TrackingSubscriberBase<Date, Never>(
receiveSubscription: {
$0.request(.max(2))
}
)
let tracking3 = TrackingSubscriberBase<Date, Never>(
receiveSubscription: {
$0.request(.max(1))
},
receiveValue: { _ in
ticks.count < 3 ? .max(1) : .none
}
)
let publisher: TimerPublisher = Timer
.publish(every: desiredInterval, on: .main, in: .default)
publisher.subscribe(tracking1)
publisher.subscribe(tracking2)
publisher.subscribe(tracking3)
XCTAssertEqual(tracking1.history, [.subscription("Timer")])
RunLoop.main.run(until: Date() + 1)
// Test that no output is produced until we connect
XCTAssertEqual(tracking1.history, [.subscription("Timer")])
let connection = publisher.connect()
RunLoop.main.run(until: Date() + 10)
assertCorrectIntervals(ticks: ticks,
expectedNumberOfTicks: 10,
desiredInterval: desiredInterval)
let fullHistory =
[TrackingSubscriberBase<Date, Never>.Event.subscription("Timer")] +
ticks.map { .value(Date(timeIntervalSinceReferenceDate: $0)) }
connection.cancel()
XCTAssert(connection is Subscription)
RunLoop.main.run(until: Date() + 1)
XCTAssertEqual(tracking1.history, fullHistory)
XCTAssertEqual(tracking2.history, fullHistory)
XCTAssertEqual(tracking3.history, fullHistory)
}
func testConnectAndCancelMultipleTimes() throws {
let publisher = TimerPublisher(interval: 0.25,
runLoop: .main,
mode: .default)
let tracking = TrackingSubscriberBase<Date, Never>()
publisher.subscribe(tracking)
let connection1 = publisher.connect()
let connection2 = publisher.connect()
XCTAssert((connection1 as AnyObject) === (connection2 as AnyObject))
connection1.cancel()
connection1.cancel()
let connection3 = try XCTUnwrap(publisher.connect() as? Subscription)
connection3.request(.max(1))
RunLoop.main.run(until: Date() + 0.3)
XCTAssertEqual(tracking.history, [.subscription("Timer")])
}
func testConnectionReflection() throws {
let publisher = TimerPublisher(interval: 0.25,
tolerance: 0.4,
runLoop: .main,
mode: .default,
options: nil)
let connection = publisher.connect()
defer { connection.cancel() }
XCTAssertEqual(
(connection as? CustomStringConvertible)?.description,
"Timer"
)
XCTAssertEqual(
(connection as? CustomPlaygroundDisplayConvertible)?
.playgroundDescription as? String,
"Timer"
)
XCTAssertFalse(connection is CustomDebugStringConvertible)
let connectionCombineID =
try XCTUnwrap(connection as? CustomCombineIdentifierConvertible)
.combineIdentifier
guard let inner = Mirror(reflecting: connection).descendant("some")
else {
XCTFail("Unexpected representation")
return
}
expectedChildren(
("downstream", "Optional(Timer)"),
("interval", "Optional(0.25)"),
("tolerance", "Optional(0.4)")
)(Mirror(reflecting: inner))
connection.cancel()
expectedChildren(
("downstream", "nil"),
("interval", "nil"),
("tolerance", "nil")
)(Mirror(reflecting: inner))
XCTAssert(inner is NSObject)
XCTAssertEqual(
(inner as? CustomStringConvertible)?.description,
"Timer"
)
XCTAssertEqual(
(inner as? CustomPlaygroundDisplayConvertible)?
.playgroundDescription as? String,
"Timer"
)
XCTAssertEqual(
(inner as? CustomDebugStringConvertible)?.debugDescription,
"Timer"
)
let innerCombineID =
try XCTUnwrap(inner as? CustomCombineIdentifierConvertible)
.combineIdentifier
XCTAssertEqual(connectionCombineID, innerCombineID)
}
private func assertCorrectIntervals(ticks: [TimeInterval],
expectedNumberOfTicks: Int,
desiredInterval: TimeInterval,
file: StaticString = #file,
line: UInt = #line) {
XCTAssertEqual(ticks.count, expectedNumberOfTicks, file: file, line: line)
if ticks.isEmpty { return }
let actualIntervals = zip(ticks.dropFirst(), ticks.dropLast()).map(-)
let averageInterval =
actualIntervals.reduce(0, +) / TimeInterval(actualIntervals.count)
XCTAssertEqual(averageInterval,
desiredInterval,
accuracy: desiredInterval / 2,
"""
Actual average interval (\(averageInterval)) deviates from \
desired interval (\(desiredInterval)) too much.
Actual intervals: \(actualIntervals)
""",
file: file,
line: line)
}
}
#if OPENCOMBINE_COMPATIBILITY_TEST || !canImport(Combine)
@available(macOS 10.15, iOS 13.0, *)
private typealias TimerPublisher = Timer.TimerPublisher
#else
private typealias TimerPublisher = Timer.OCombine.TimerPublisher
#endif
@@ -587,7 +587,7 @@ private final class TestURLSessionDataTask: URLSessionDataTask {
#if canImport(Darwin)
return super.earliestBeginDate
#else
return nil // Deprecated in swift-corerlibs-foundation
return nil // Deprecated in swift-corelibs-foundation
#endif
}
set {
@@ -58,7 +58,7 @@ extension XCTest {
environment[childProcessEnvVariable] = childProcessEnvVariableOnValue
childProcess.environment = environment
func printDiagostics() {
func printDiagnostics() {
print("Parent process invocation:")
print(ProcessInfo.processInfo.arguments.joined(separator: " "))
print("Child process invocation:")
@@ -73,13 +73,13 @@ extension XCTest {
childProcess.waitUntilExit()
if childProcess.terminationReason != .uncaughtSignal {
XCTFail("Child process should have crashed: \(childProcess)")
printDiagostics()
printDiagnostics()
}
} catch {
XCTFail("""
Couldn't start child process for testing crash: \(childProcess) - \(error)
""")
printDiagostics()
printDiagnostics()
}
}
#endif
@@ -5,7 +5,7 @@
// Created by Sergej Jaskiewicz on 02.12.2019.
//
/// A priproty queue based on binary min-heap.
/// A priority 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> {
@@ -26,8 +26,8 @@ struct FairPriorityQueue<Priority: Comparable, Element> {
}
}
func min() -> Element? {
return storage.first?.1
func min() -> (Priority, Element)? {
return storage.first.map { ($0.0.0, $0.1) }
}
@discardableResult
@@ -33,7 +33,7 @@ func testLifecycle<UpstreamOutput, Operator: Publisher>(
let emptySubscriber =
TrackingSubscriberBase<Operator.Output, Operator.Failure>(onDeinit: onDeinit)
XCTAssertTrue(emptySubscriber.history.isEmpty,
"Lifecycle test #1: thesubscriber's history should be empty",
"Lifecycle test #1: the subscriber's history should be empty",
file: file,
line: line)
operatorPublisher.subscribe(emptySubscriber)
@@ -212,3 +212,24 @@ internal func testSubscriptionReflection<Sut: Publisher>(
line: line
)
}
/// Prior to iOS 14 there was a bug in PassthroughSubject and
/// CurrentValueSubject when after cancelling the subscription we couldn't
/// reflect the subscription.
@available(macOS, deprecated: 10.16, message: """
If macOS 10.16/11.0 has already been released, this property should be removed
""")
@available(iOS, deprecated: 14, message: """
If iOS 14 has already been released, this property should be removed
""")
var hasCustomMirrorUseAfterFreeBug: Bool { // swiftlint:disable:this let_var_whitespace
#if OPENCOMBINE_COMPATIBILITY_TEST
if #available(macOS 10.16, iOS 14.0, *) {
return false
} else {
return true
}
#else
return false
#endif
}
@@ -11,6 +11,14 @@ import Combine
import OpenCombine
#endif
@available(macOS 10.15, iOS 13.0, *)
protocol CancellableTokenProtocol: Cancellable {
init(_ scheduler: VirtualTimeScheduler)
var isCancelled: Bool { get }
}
@available(macOS 10.15, iOS 13.0, *)
final class VirtualTimeScheduler: Scheduler {
@@ -171,15 +179,42 @@ final class VirtualTimeScheduler: Scheduler {
}
}
private final class CancellableToken: Cancellable {
final class CancellableToken: CancellableTokenProtocol {
weak var scheduler: VirtualTimeScheduler?
private(set) var isCancelled = false
init(_ scheduler: VirtualTimeScheduler) {
self.scheduler = scheduler
}
deinit {
scheduler?.cancellableTokenDeinitCount += 1
}
func cancel() {
isCancelled = true
}
}
final class NoopCancellableToken: CancellableTokenProtocol {
weak var scheduler: VirtualTimeScheduler?
init(_ scheduler: VirtualTimeScheduler) {
self.scheduler = scheduler
}
deinit {
scheduler?.cancellableTokenDeinitCount += 1
}
var isCancelled: Bool { return false }
func cancel() {}
}
enum Event: Equatable, CustomStringConvertible {
case now
case minimumTolerance
@@ -221,7 +256,7 @@ final class VirtualTimeScheduler: Scheduler {
"""
case let .scheduleAfterDateWithInterval(date, interval, tolerance, options):
return """
.scheduleAfterDateWithInterval(\(describeDate(date)), \
.scheduleAfterDateWithInterval(\(describeDate(date))), \
interval: \(describeStride(interval)), \
tolerance: \(describeStride(tolerance)), \
options: \(describeOptions(options)))
@@ -239,6 +274,14 @@ final class VirtualTimeScheduler: Scheduler {
private var workQueue = FairPriorityQueue<SchedulerTimeType, () -> Void>()
private let cancellableTokenType: CancellableTokenProtocol.Type
fileprivate(set) var cancellableTokenDeinitCount = 0
init(cancellableTokenType: CancellableTokenProtocol.Type = CancellableToken.self) {
self.cancellableTokenType = cancellableTokenType
}
var scheduledDates: [SchedulerTimeType] {
return workQueue.map { $0.0 }
}
@@ -250,7 +293,7 @@ final class VirtualTimeScheduler: Scheduler {
var minimumTolerance: SchedulerTimeType.Stride {
history.append(.minimumTolerance)
return 0
return .nanoseconds(7)
}
func schedule(options: SchedulerOptions?, _ action: @escaping () -> Void) {
@@ -275,7 +318,7 @@ final class VirtualTimeScheduler: Scheduler {
interval: interval,
tolerance: tolerance,
options: options))
let cancellableToken = CancellableToken()
let cancellableToken = cancellableTokenType.init(self)
repeatedlyExecute(after: date,
interval: interval,
cancellableToken: cancellableToken,
@@ -285,7 +328,7 @@ final class VirtualTimeScheduler: Scheduler {
private func repeatedlyExecute(after date: SchedulerTimeType,
interval: SchedulerTimeType.Stride,
cancellableToken: CancellableToken,
cancellableToken: CancellableTokenProtocol,
action: @escaping () -> Void) {
let enqueuedAction: () -> Void = { [unowned self] in
if cancellableToken.isCancelled { return }
@@ -304,11 +347,20 @@ final class VirtualTimeScheduler: Scheduler {
/// - 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) {
if time > _now {
while let (nextActionTime, action) = workQueue.min(), nextActionTime <= time {
workQueue.extractMin()
_now = max(nextActionTime, _now)
action()
}
}
_now = time
}
func executeScheduledActions() {
while let (time, action) = workQueue.extractMin() {
func executeScheduledActions(until deadline: SchedulerTimeType = .nanoseconds(.max)) {
precondition(deadline >= _now)
while let (time, action) = workQueue.min(), time <= deadline {
workQueue.extractMin()
_now = max(time, _now)
action()
}
@@ -105,18 +105,18 @@ final class PassthroughSubjectTests: XCTestCase {
}
func testSendFailureCompletion() {
let cvs = Sut()
let passthrough = Sut()
let subscriber = TrackingSubscriber(
receiveSubscription: { subscription in
subscription.request(.unlimited)
}
)
cvs.subscribe(subscriber)
passthrough.subscribe(subscriber)
XCTAssertEqual(subscriber.history, [.subscription("PassthroughSubject")])
cvs.send(completion: .failure(.oops))
passthrough.send(completion: .failure(.oops))
XCTAssertEqual(subscriber.history, [.subscription("PassthroughSubject"),
.completion(.failure(.oops))])
@@ -173,6 +173,22 @@ final class PassthroughSubjectTests: XCTestCase {
XCTAssertEqual(subscriber.subscriptions.count, 11)
XCTAssertEqual(subscriber.inputs.count, 0)
XCTAssertEqual(subscriber.completions.count, 0)
passthrough.send(0)
XCTAssertEqual(subscriber.inputs, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
for (i, subscription) in subscriber.subscriptions.enumerated()
where i.isMultiple(of: 2)
{
subscription.cancel()
}
passthrough.send(1)
XCTAssertEqual(
subscriber.inputs,
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1]
)
}
// Reactive Streams Spec: Rule #6
@@ -310,6 +326,97 @@ final class PassthroughSubjectTests: XCTestCase {
XCTAssertEqual(subscription2.history, [.requested(.unlimited)])
}
func testCompletion() throws {
let passthrough = Sut()
var downstreamSubscription: Subscription?
let tracking = TrackingSubscriber(
receiveSubscription: { downstreamSubscription = $0 }
)
passthrough.subscribe(tracking)
try XCTUnwrap(downstreamSubscription).request(.max(12))
passthrough.send(1)
expectedChildren(
("parent", .contains("PassthroughSubject")),
("downstream", .contains("TrackingSubscriberBase")),
("demand", "max(11)"),
("subject", .contains("PassthroughSubject"))
)(Mirror(reflecting: try XCTUnwrap(downstreamSubscription)))
passthrough.send(completion: .finished)
if !hasCustomMirrorUseAfterFreeBug {
expectedChildren(
("parent", "nil"),
("downstream", "nil"),
("demand", "max(11)"),
("subject", "nil")
)(Mirror(reflecting: try XCTUnwrap(downstreamSubscription)))
}
XCTAssertEqual(tracking.history, [.subscription("PassthroughSubject"),
.value(1),
.completion(.finished)])
passthrough.send(completion: .failure(.oops))
try XCTUnwrap(downstreamSubscription).cancel()
try XCTUnwrap(downstreamSubscription).request(.max(3))
if !hasCustomMirrorUseAfterFreeBug {
expectedChildren(
("parent", "nil"),
("downstream", "nil"),
("demand", "max(11)"),
("subject", "nil")
)(Mirror(reflecting: try XCTUnwrap(downstreamSubscription)))
}
XCTAssertEqual(tracking.history, [.subscription("PassthroughSubject"),
.value(1),
.completion(.finished)])
}
func testCancellation() throws {
let passthrough = Sut()
var downstreamSubscription: Subscription?
let tracking = TrackingSubscriber(
receiveSubscription: { downstreamSubscription = $0 }
)
passthrough.subscribe(tracking)
try XCTUnwrap(downstreamSubscription).request(.max(12))
passthrough.send(1)
expectedChildren(
("parent", .contains("PassthroughSubject")),
("downstream", .contains("TrackingSubscriberBase")),
("demand", "max(11)"),
("subject", .contains("PassthroughSubject"))
)(Mirror(reflecting: try XCTUnwrap(downstreamSubscription)))
try XCTUnwrap(downstreamSubscription).cancel()
try XCTUnwrap(downstreamSubscription).cancel()
try XCTUnwrap(downstreamSubscription).request(.max(3))
try XCTUnwrap(downstreamSubscription).request(.max(4))
if !hasCustomMirrorUseAfterFreeBug {
expectedChildren(
("parent", "nil"),
("downstream", "nil"),
("demand", "max(11)"),
("subject", "nil")
)(Mirror(reflecting: try XCTUnwrap(downstreamSubscription)))
}
XCTAssertEqual(tracking.history, [.subscription("PassthroughSubject"),
.value(1)])
}
func testLifecycle() throws {
var deinitCounter = 0
@@ -364,67 +471,76 @@ final class PassthroughSubjectTests: XCTestCase {
XCTAssertEqual(deinitCounter, 2)
}
func testSynchronization() {
let subscriptions = Atomic<[Subscription]>([])
let inputs = Atomic<[Int]>([])
let completions = Atomic<[Subscribers.Completion<TestingError>]>([])
let passthrough = Sut()
let subscriber = AnySubscriber<Int, TestingError>(
receiveSubscription: { subscription in
subscriptions.do { $0.append(subscription) }
subscription.request(.unlimited)
},
receiveValue: { value in
inputs.do { $0.append(value) }
return .none
},
receiveCompletion: { completion in
completions.do { $0.append(completion) }
func testCancelsUpstreamSubscriptionsOnDeinit() {
let subscription = CustomSubscription()
do {
let passthrough = Sut()
for _ in 0 ..< 5 {
passthrough.send(subscription: subscription)
}
)
XCTAssertEqual(subscription.history, [])
}
race(
{
passthrough.subscribe(subscriber)
},
{
passthrough.subscribe(subscriber)
XCTAssertEqual(subscription.history, [.cancelled,
.cancelled,
.cancelled,
.cancelled,
.cancelled])
}
func testReleasesEverythingOnTermination() {
enum TerminationReason: CaseIterable {
case cancelled
case finished
case failed
}
for reason in TerminationReason.allCases {
weak var weakSubscriber: TrackingSubscriber?
weak var weakSubject: Sut?
weak var weakSubscription: AnyObject?
do {
let subject = Sut()
do {
let subscriber = TrackingSubscriber(receiveSubscription: {
weakSubscription = $0 as AnyObject
})
weakSubscriber = subscriber
weakSubject = subject
subject.subscribe(subscriber)
}
switch reason {
case .cancelled:
(weakSubscription as? Subscription)?.cancel()
case .finished:
subject.send(completion: .finished)
case .failed:
subject.send(completion: .failure(.oops))
}
XCTAssertNil(weakSubscriber, "Subscriber leaked - \(reason)")
XCTAssertNil(weakSubscription, "Subscription leaked - \(reason)")
}
XCTAssertNil(weakSubject, "Subject leaked - \(reason)")
}
}
func testConduitReflection() throws {
try testSubscriptionReflection(
description: "PassthroughSubject",
customMirror: expectedChildren(
("parent", .contains("PassthroughSubject")),
("downstream", .contains("TrackingSubscriberBase")),
("demand", "max(0)"),
("subject", .contains("PassthroughSubject"))
),
playgroundDescription: "PassthroughSubject",
sut: PassthroughSubject<Int, Error>()
)
XCTAssertEqual(subscriptions.value.count, 200)
race(
{
passthrough.send(31)
},
{
passthrough.send(42)
}
)
XCTAssertEqual(inputs.value.count, 40000)
race(
{
subscriptions.value[0].request(.max(4))
},
{
subscriptions.value[0].request(.max(10))
}
)
race(
{
passthrough.send(completion: .finished)
},
{
passthrough.send(completion: .failure(""))
}
)
XCTAssertEqual(completions.value.count, 200)
}
}
@@ -18,14 +18,14 @@ final class BufferTests: XCTestCase {
func testInitialDemandWithKeepFullPrefetchStrategy() {
testInitialDemand(
withPrefetchStragety: .keepFull,
withPrefetchStrategy: .keepFull,
expectedSubscriptionHistory: [.requested(.max(42))]
)
}
func testInitialDemandWithByRequestPrefetchStrategy() {
testInitialDemand(
withPrefetchStragety: .byRequest,
withPrefetchStrategy: .byRequest,
expectedSubscriptionHistory: [.requested(.unlimited)]
)
}
@@ -379,7 +379,7 @@ final class BufferTests: XCTestCase {
// MARK: - Generic tests
private func testInitialDemand(
withPrefetchStragety prefetch: Publishers.PrefetchStrategy,
withPrefetchStrategy prefetch: Publishers.PrefetchStrategy,
expectedSubscriptionHistory: [CustomSubscription.Event]
) {
let subscription = CustomSubscription()
@@ -29,7 +29,7 @@ final class CollectTests: XCTestCase {
func testtestUpstreamFinishesImmediately() {
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "Collect",
expectedResult: [],
expectedResult: [Int](),
{ $0.collect() })
}
@@ -0,0 +1,630 @@
//
// DebounceTests.swift
//
//
// Created by Sergej Jaskiewicz on 28.06.2020.
//
import XCTest
#if OPENCOMBINE_COMPATIBILITY_TEST
import Combine
#else
import OpenCombine
#endif
@available(macOS 10.15, iOS 13.0, *)
final class DebounceTests: XCTestCase {
func testBasicBehavior() {
let scheduler = VirtualTimeScheduler()
let helper = OperatorTestHelper(
publisherType: CustomPublisher.self,
initialDemand: .max(2),
receiveValueDemand: .max(1),
createSut: {
$0.debounce(for: .nanoseconds(13),
scheduler: scheduler,
options: .nontrivialOptions)
}
)
XCTAssertEqual(helper.tracking.history, [.subscription("Debounce")])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(scheduler.history, [])
XCTAssertEqual(helper.publisher.send(1), .none)
XCTAssertEqual(helper.tracking.history, [.subscription("Debounce")])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(scheduler.history,
[.now,
.minimumTolerance,
.scheduleAfterDateWithInterval(.nanoseconds(13),
interval: .nanoseconds(13),
tolerance: .nanoseconds(7),
options: .nontrivialOptions)])
scheduler.rewind(to: .nanoseconds(4))
XCTAssertEqual(helper.publisher.send(2), .none)
scheduler.rewind(to: .nanoseconds(9))
XCTAssertEqual(helper.publisher.send(3), .none)
scheduler.rewind(to: .nanoseconds(200))
XCTAssertEqual(helper.tracking.history, [.subscription("Debounce"),
.value(3)])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(scheduler.history,
[.now,
.minimumTolerance,
.scheduleAfterDateWithInterval(.nanoseconds(13),
interval: .nanoseconds(13),
tolerance: .nanoseconds(7),
options: .nontrivialOptions),
.now,
.minimumTolerance,
.scheduleAfterDateWithInterval(.nanoseconds(17),
interval: .nanoseconds(13),
tolerance: .nanoseconds(7),
options: .nontrivialOptions),
.now,
.minimumTolerance,
.scheduleAfterDateWithInterval(.nanoseconds(22),
interval: .nanoseconds(13),
tolerance: .nanoseconds(7),
options: .nontrivialOptions)])
helper.publisher.send(completion: .finished)
helper.publisher.send(completion: .failure(.oops)) // ignored
XCTAssertEqual(helper.publisher.send(-1), .none) // ignored
XCTAssertEqual(helper.tracking.history, [.subscription("Debounce"),
.value(3)])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(scheduler.history,
[.now,
.minimumTolerance,
.scheduleAfterDateWithInterval(.nanoseconds(13),
interval: .nanoseconds(13),
tolerance: .nanoseconds(7),
options: .nontrivialOptions),
.now,
.minimumTolerance,
.scheduleAfterDateWithInterval(.nanoseconds(17),
interval: .nanoseconds(13),
tolerance: .nanoseconds(7),
options: .nontrivialOptions),
.now,
.minimumTolerance,
.scheduleAfterDateWithInterval(.nanoseconds(22),
interval: .nanoseconds(13),
tolerance: .nanoseconds(7),
options: .nontrivialOptions),
.schedule(options: nil)])
scheduler.rewind(to: .nanoseconds(300))
XCTAssertEqual(helper.tracking.history, [.subscription("Debounce"),
.value(3),
.completion(.finished)])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(scheduler.history,
[.now,
.minimumTolerance,
.scheduleAfterDateWithInterval(.nanoseconds(13),
interval: .nanoseconds(13),
tolerance: .nanoseconds(7),
options: .nontrivialOptions),
.now,
.minimumTolerance,
.scheduleAfterDateWithInterval(.nanoseconds(17),
interval: .nanoseconds(13),
tolerance: .nanoseconds(7),
options: .nontrivialOptions),
.now,
.minimumTolerance,
.scheduleAfterDateWithInterval(.nanoseconds(22),
interval: .nanoseconds(13),
tolerance: .nanoseconds(7),
options: .nontrivialOptions),
.schedule(options: nil)])
XCTAssertEqual(scheduler.cancellableTokenDeinitCount, 2)
}
func testFinishBeforeDue() {
let scheduler = VirtualTimeScheduler()
let helper = OperatorTestHelper(
publisherType: CustomPublisher.self,
initialDemand: .max(1),
receiveValueDemand: .none,
createSut: {
$0.debounce(for: .nanoseconds(13),
scheduler: scheduler,
options: .nontrivialOptions)
}
)
XCTAssertEqual(helper.publisher.send(1), .none)
scheduler.rewind(to: .nanoseconds(4))
helper.publisher.send(completion: .failure(.oops))
XCTAssertEqual(helper.tracking.history, [.subscription("Debounce")])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(scheduler.history,
[
.now,
.minimumTolerance,
.scheduleAfterDateWithInterval(.nanoseconds(13),
interval: .nanoseconds(13),
tolerance: .nanoseconds(7),
options: .nontrivialOptions),
.schedule(options: nil)])
scheduler.rewind(to: .nanoseconds(100))
XCTAssertEqual(helper.tracking.history, [.subscription("Debounce"),
.completion(.failure(.oops))])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(scheduler.history,
[
.now,
.minimumTolerance,
.scheduleAfterDateWithInterval(.nanoseconds(13),
interval: .nanoseconds(13),
tolerance: .nanoseconds(7),
options: .nontrivialOptions),
.schedule(options: nil)])
XCTAssertEqual(scheduler.cancellableTokenDeinitCount, 0)
}
func testFailBeforeDue() {
let scheduler = VirtualTimeScheduler()
let helper = OperatorTestHelper(
publisherType: CustomPublisher.self,
initialDemand: .max(1),
receiveValueDemand: .none,
createSut: {
$0.debounce(for: .nanoseconds(13),
scheduler: scheduler,
options: .nontrivialOptions)
}
)
XCTAssertEqual(helper.publisher.send(1), .none)
scheduler.rewind(to: .nanoseconds(4))
helper.publisher.send(completion: .finished)
XCTAssertEqual(helper.tracking.history, [.subscription("Debounce")])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(scheduler.history,
[
.now,
.minimumTolerance,
.scheduleAfterDateWithInterval(.nanoseconds(13),
interval: .nanoseconds(13),
tolerance: .nanoseconds(7),
options: .nontrivialOptions),
.schedule(options: nil)])
scheduler.rewind(to: .nanoseconds(100))
XCTAssertEqual(helper.tracking.history, [.subscription("Debounce"),
.completion(.finished)])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(scheduler.history,
[
.now,
.minimumTolerance,
.scheduleAfterDateWithInterval(.nanoseconds(13),
interval: .nanoseconds(13),
tolerance: .nanoseconds(7),
options: .nontrivialOptions),
.schedule(options: nil)])
XCTAssertEqual(scheduler.cancellableTokenDeinitCount, 0)
}
func testCancelBeforeDue() throws {
let scheduler = VirtualTimeScheduler()
let helper = OperatorTestHelper(
publisherType: CustomPublisher.self,
initialDemand: .max(1),
receiveValueDemand: .none,
createSut: {
$0.debounce(for: .nanoseconds(13),
scheduler: scheduler,
options: .nontrivialOptions)
}
)
XCTAssertEqual(helper.publisher.send(1), .none)
scheduler.rewind(to: .nanoseconds(4))
try XCTUnwrap(helper.downstreamSubscription).cancel()
XCTAssertEqual(helper.tracking.history, [.subscription("Debounce")])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited), .cancelled])
XCTAssertEqual(scheduler.history,
[.now,
.minimumTolerance,
.scheduleAfterDateWithInterval(.nanoseconds(13),
interval: .nanoseconds(13),
tolerance: .nanoseconds(7),
options: .nontrivialOptions)])
scheduler.rewind(to: .nanoseconds(100))
XCTAssertEqual(helper.tracking.history, [.subscription("Debounce")])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited), .cancelled])
XCTAssertEqual(scheduler.history,
[.now,
.minimumTolerance,
.scheduleAfterDateWithInterval(.nanoseconds(13),
interval: .nanoseconds(13),
tolerance: .nanoseconds(7),
options: .nontrivialOptions)])
XCTAssertEqual(scheduler.cancellableTokenDeinitCount, 0)
}
func testDemand() throws {
let scheduler = VirtualTimeScheduler()
let helper = OperatorTestHelper(
publisherType: CustomPublisher.self,
initialDemand: nil,
receiveValueDemand: .none,
createSut: {
$0.debounce(for: .nanoseconds(13),
scheduler: scheduler,
options: .nontrivialOptions)
}
)
XCTAssertEqual(helper.tracking.history, [.subscription("Debounce")])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(scheduler.history, [])
XCTAssertEqual(helper.publisher.send(1), .none)
XCTAssertEqual(helper.tracking.history, [.subscription("Debounce")])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(scheduler.history,
[.now,
.minimumTolerance,
.scheduleAfterDateWithInterval(.nanoseconds(13),
interval: .nanoseconds(13),
tolerance: .nanoseconds(7),
options: .nontrivialOptions)])
scheduler.rewind(to: .nanoseconds(100))
XCTAssertEqual(helper.tracking.history, [.subscription("Debounce")])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(scheduler.history,
[.now,
.minimumTolerance,
.scheduleAfterDateWithInterval(.nanoseconds(13),
interval: .nanoseconds(13),
tolerance: .nanoseconds(7),
options: .nontrivialOptions)])
try XCTUnwrap(helper.downstreamSubscription).request(.max(3))
XCTAssertEqual(helper.tracking.history, [.subscription("Debounce")])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(scheduler.history,
[.now,
.minimumTolerance,
.scheduleAfterDateWithInterval(.nanoseconds(13),
interval: .nanoseconds(13),
tolerance: .nanoseconds(7),
options: .nontrivialOptions)])
scheduler.rewind(to: .nanoseconds(200))
XCTAssertEqual(helper.tracking.history, [.subscription("Debounce")])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(scheduler.history,
[.now,
.minimumTolerance,
.scheduleAfterDateWithInterval(.nanoseconds(13),
interval: .nanoseconds(13),
tolerance: .nanoseconds(7),
options: .nontrivialOptions)])
XCTAssertEqual(helper.publisher.send(2), .none)
scheduler.rewind(to: .nanoseconds(250))
XCTAssertEqual(helper.publisher.send(3), .none)
scheduler.rewind(to: .nanoseconds(300))
XCTAssertEqual(helper.publisher.send(4), .none)
scheduler.rewind(to: .nanoseconds(350))
XCTAssertEqual(helper.publisher.send(5), .none)
scheduler.rewind(to: .nanoseconds(400))
XCTAssertEqual(helper.tracking.history, [.subscription("Debounce"),
.value(2),
.value(3),
.value(4)])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(scheduler.history,
[.now,
.minimumTolerance,
.scheduleAfterDateWithInterval(.nanoseconds(13),
interval: .nanoseconds(13),
tolerance: .nanoseconds(7),
options: .nontrivialOptions),
.now,
.minimumTolerance,
.scheduleAfterDateWithInterval(.nanoseconds(213),
interval: .nanoseconds(13),
tolerance: .nanoseconds(7),
options: .nontrivialOptions),
.now,
.minimumTolerance,
.scheduleAfterDateWithInterval(.nanoseconds(263),
interval: .nanoseconds(13),
tolerance: .nanoseconds(7),
options: .nontrivialOptions),
.now,
.minimumTolerance,
.scheduleAfterDateWithInterval(.nanoseconds(313),
interval: .nanoseconds(13),
tolerance: .nanoseconds(7),
options: .nontrivialOptions),
.now,
.minimumTolerance,
.scheduleAfterDateWithInterval(.nanoseconds(363),
interval: .nanoseconds(13),
tolerance: .nanoseconds(7),
options: .nontrivialOptions)])
try XCTUnwrap(helper.downstreamSubscription).cancel()
try XCTUnwrap(helper.downstreamSubscription).request(.max(1))
XCTAssertEqual(helper.publisher.send(6), .none)
scheduler.rewind(to: .nanoseconds(450))
XCTAssertEqual(helper.tracking.history, [.subscription("Debounce"),
.value(2),
.value(3),
.value(4)])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited), .cancelled])
XCTAssertEqual(scheduler.history,
[.now,
.minimumTolerance,
.scheduleAfterDateWithInterval(.nanoseconds(13),
interval: .nanoseconds(13),
tolerance: .nanoseconds(7),
options: .nontrivialOptions),
.now,
.minimumTolerance,
.scheduleAfterDateWithInterval(.nanoseconds(213),
interval: .nanoseconds(13),
tolerance: .nanoseconds(7),
options: .nontrivialOptions),
.now,
.minimumTolerance,
.scheduleAfterDateWithInterval(.nanoseconds(263),
interval: .nanoseconds(13),
tolerance: .nanoseconds(7),
options: .nontrivialOptions),
.now,
.minimumTolerance,
.scheduleAfterDateWithInterval(.nanoseconds(313),
interval: .nanoseconds(13),
tolerance: .nanoseconds(7),
options: .nontrivialOptions),
.now,
.minimumTolerance,
.scheduleAfterDateWithInterval(.nanoseconds(363),
interval: .nanoseconds(13),
tolerance: .nanoseconds(7),
options: .nontrivialOptions)])
XCTAssertEqual(scheduler.cancellableTokenDeinitCount, 4)
}
func testBadScheduler() {
// What if the scheduler returns a cancellable that does nothing at all?
let scheduler = VirtualTimeScheduler(
cancellableTokenType: VirtualTimeScheduler.NoopCancellableToken.self
)
let helper = OperatorTestHelper(
publisherType: CustomPublisher.self,
initialDemand: .unlimited,
receiveValueDemand: .none,
createSut: {
$0.debounce(for: .nanoseconds(13),
scheduler: scheduler,
options: .nontrivialOptions)
}
)
XCTAssertEqual(helper.publisher.send(1), .none)
scheduler.rewind(to: .nanoseconds(2))
XCTAssertEqual(helper.publisher.send(2), .none)
scheduler.rewind(to: .nanoseconds(4))
XCTAssertEqual(helper.publisher.send(3), .none)
scheduler.rewind(to: .nanoseconds(6))
XCTAssertEqual(helper.publisher.send(42), .none)
scheduler.rewind(to: .nanoseconds(50))
XCTAssertEqual(helper.tracking.history, [.subscription("Debounce"),
.value(42),
.value(42),
.value(42)])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(scheduler.history,
[.now,
.minimumTolerance,
.scheduleAfterDateWithInterval(.nanoseconds(13),
interval: .nanoseconds(13),
tolerance: .nanoseconds(7),
options: .nontrivialOptions),
.now,
.minimumTolerance,
.scheduleAfterDateWithInterval(.nanoseconds(15),
interval: .nanoseconds(13),
tolerance: .nanoseconds(7),
options: .nontrivialOptions),
.now,
.minimumTolerance,
.scheduleAfterDateWithInterval(.nanoseconds(17),
interval: .nanoseconds(13),
tolerance: .nanoseconds(7),
options: .nontrivialOptions),
.now,
.minimumTolerance,
.scheduleAfterDateWithInterval(.nanoseconds(19),
interval: .nanoseconds(13),
tolerance: .nanoseconds(7),
options: .nontrivialOptions)])
XCTAssertEqual(scheduler.cancellableTokenDeinitCount, 0)
}
func testSetupTimerWeakCapture() {
let scheduler = VirtualTimeScheduler()
var subscriptionDestroyed = false
do {
let helper = OperatorTestHelper(
publisherType: CustomPublisher.self,
initialDemand: .unlimited,
receiveValueDemand: .none,
createSut: {
$0.debounce(for: .nanoseconds(13), scheduler: scheduler)
}
)
helper.tracking.onDeinit = { subscriptionDestroyed = true }
XCTAssertEqual(helper.publisher.send(1), .none)
}
XCTAssertTrue(subscriptionDestroyed)
}
func testCrashesWithImmediateScheduler() {
let helper = OperatorTestHelper(
publisherType: CustomPublisher.self,
initialDemand: .max(2),
receiveValueDemand: .max(1),
createSut: {
$0.debounce(for: .nanoseconds(13),
scheduler: ImmediateScheduler.shared)
}
)
assertCrashes {
_ = helper.publisher.send(1)
}
}
func testTimeoutReceiveValueBeforeSubscription() {
let scheduler = VirtualTimeScheduler()
testReceiveValueBeforeSubscription(
value: 42,
expected: .crash,
{ $0.debounce(for: .nanoseconds(13), scheduler: scheduler) }
)
}
func testTimeoutReceiveCompletionBeforeSubscription() {
let scheduler = VirtualTimeScheduler()
testReceiveCompletionBeforeSubscription(
inputType: Int.self,
expected: .crash,
{ $0.debounce(for: .nanoseconds(13), scheduler: scheduler) }
)
}
func testTimeoutRequestBeforeSubscription() {
let scheduler = VirtualTimeScheduler()
testRequestBeforeSubscription(
inputType: Int.self,
shouldCrash: true,
{ $0.debounce(for: .nanoseconds(13), scheduler: scheduler) }
)
}
func testTimeoutReceiveSubscriptionTwice() throws {
let scheduler = VirtualTimeScheduler()
let helper = OperatorTestHelper(
publisherType: CustomPublisher.self,
initialDemand: nil,
receiveValueDemand: .none,
createSut: { $0.debounce(for: .nanoseconds(13), scheduler: scheduler) }
)
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 testTimeoutCancelBeforeSubscription() {
let scheduler = VirtualTimeScheduler()
testCancelBeforeSubscription(
inputType: Int.self,
shouldCrash: false,
{ $0.timeout(.nanoseconds(13), scheduler: scheduler) }
)
}
func testDebounceReflection() throws {
let scheduler = VirtualTimeScheduler()
let customMirror = hasUpdatedReflection
? expectedChildren(
("downstream", .contains("TrackingSubscriberBase")),
("downstreamDemand", "max(0)"),
("currentValue", "nil")
)
: expectedChildren(
("upstream", .contains("CustomConnectablePublisherBase")),
("downstream", .contains("TrackingSubscriberBase")),
("upstreamSubscription", .anything),
("downstreamDemand", "max(0)"),
("currentValue", "nil")
)
try testReflection(
parentInput: Int.self,
parentFailure: Error.self,
description: "Debounce",
customMirror: customMirror,
playgroundDescription: "Debounce",
{ $0.debounce(for: .nanoseconds(13), scheduler: scheduler) }
)
}
}
// FIXME: Remove this as soon as we switch our CI to the latest OS releases
private var hasUpdatedReflection: Bool {
#if OPENCOMBINE_COMPATIBILITY_TEST
if #available(macOS 10.16, iOS 14.0, *) {
return true
} else {
return false
}
#else
return true
#endif
}
@@ -372,7 +372,7 @@ final class DelayTests: XCTestCase {
[.minimumTolerance,
.now,
.scheduleAfterDate(.seconds(0.35),
tolerance: 0,
tolerance: .nanoseconds(7),
options: nil)])
tracking.cancel()
publisher.cancel()
@@ -401,7 +401,7 @@ final class DelayTests: XCTestCase {
[.minimumTolerance,
.now,
.scheduleAfterDate(.seconds(0.35),
tolerance: 0,
tolerance: .nanoseconds(7),
options: nil)])
tracking.cancel()
publisher.cancel()
@@ -115,7 +115,7 @@ final class FlatMapTests: XCTestCase {
// 1. FlatMap.Inner.receive(_ input:)
// 2. Publisher.subscribe
// ...
// 3. FlatMap.Inner.ChildSubscriber.recive(subscription:)
// 3. FlatMap.Inner.ChildSubscriber.receive(subscription:)
// 4. subscription.request()
// 5. Just.Inner.request()
// 6. FlatMap.Inner.child(_:receivedValue)
@@ -139,7 +139,7 @@ final class FlatMapTests: XCTestCase {
flatMap.subscribe(downstreamSubscriber)
XCTAssertEqual(upstreamPublisher.send(666), .none)
// Simply making it here shows that there's no dealock
// Simply making it here shows that there's no deadlock
}
func testCancelCancels() throws {
@@ -965,9 +965,9 @@ final class FlatMapTests: XCTestCase {
.requested(.max(1))])
}
func testSendsSubcriptionDownstreamBeforeDemandUpstream() {
func testSendsSubscriptionDownstreamBeforeDemandUpstream() {
let sentDemandRequestUpstream = "Sent demand request upstream"
let sentSubscriptionDownstream = "Sent subcription downstream"
let sentSubscriptionDownstream = "Sent subscription downstream"
var receiveOrder: [String] = []
let upstreamSubscription = CustomSubscription(onRequest: { _ in
@@ -98,6 +98,8 @@ final class FutureTests: XCTestCase {
future.subscribe(subscriber)
subscriber.subscriptions.forEach { $0.cancel() }
subscriber.subscriptions.forEach { $0.cancel() }
subscriber.subscriptions.forEach { $0.request(.max(1)) }
promise?(.success(42))
@@ -106,7 +108,7 @@ final class FutureTests: XCTestCase {
])
}
func testSubscribeAfterResolution() {
func testSubscribeAfterSuccessfulResolution() {
var promise: Sut.Promise?
let future = Sut { promise = $0 }
@@ -124,15 +126,36 @@ final class FutureTests: XCTestCase {
])
}
func testCrashesOnZeroDemand() {
func testSubscribeAfterFailure() {
// TODO: Remove this `if` as soon as iOS 14 is released.
if hasMissingFailureAfterLateSubscriptionBug { return }
var promise: Sut.Promise?
let future = Sut { promise = $0 }
promise?(.failure(.oops))
let subscriber = TrackingSubscriber()
future.subscribe(subscriber)
XCTAssertEqual(subscriber.history, [.subscription("Future"),
.completion(.failure(.oops))])
}
func testCrashesOnZeroDemand() throws {
let future = Sut { _ in }
let subscriber = TrackingSubscriber(receiveSubscription: { subscription in
self.assertCrashes {
subscription.request(.none)
var downstreamSubscription: Subscription?
let subscriber = TrackingSubscriber(
receiveSubscription: {
downstreamSubscription = $0
}
})
)
future.subscribe(subscriber)
try self.assertCrashes {
try XCTUnwrap(downstreamSubscription).request(.none)
}
}
func testValueRecursion() {
@@ -211,7 +234,7 @@ final class FutureTests: XCTestCase {
XCTAssertTrue(hasStarted)
}
func testWaitsForRequest() {
func testWaitsForDemandSuccess() {
var promise: Sut.Promise?
let future = Sut { promise = $0 }
@@ -236,4 +259,105 @@ final class FutureTests: XCTestCase {
.completion(.finished)
])
}
func testReleasesEverythingOnTermination() {
enum TerminationReason: CaseIterable {
case cancelled
case finished
case failed
}
for reason in TerminationReason.allCases {
weak var weakSubscriber: TrackingSubscriber?
weak var weakFuture: Sut?
weak var weakSubscription: AnyObject?
do {
var promise: Sut.Promise?
let future = Sut { promise = $0 }
do {
let subscriber = TrackingSubscriber(
receiveSubscription: {
weakSubscription = $0 as AnyObject
$0.request(.max(1))
}
)
weakSubscriber = subscriber
weakFuture = future
future.subscribe(subscriber)
}
switch reason {
case .cancelled:
(weakSubscription as? Subscription)?.cancel()
case .finished:
promise?(.success(1))
case .failed:
promise?(.failure(.oops))
}
XCTAssertNil(weakSubscriber, "Subscriber leaked - \(reason)")
if !leaksSubscription {
// This leak has been fixed in the betas.
// TODO: Remove this `if` as soon as iOS 14 is released.
XCTAssertNil(weakSubscription, "Subscription leaked - \(reason)")
}
}
XCTAssertNil(weakFuture, "Future leaked - \(reason)")
}
}
func testConduitReflection() throws {
try testSubscriptionReflection(
description: "Future",
customMirror: expectedChildren(
("parent", .contains("Future")),
("downstream", .contains("TrackingSubscriberBase")),
("hasAnyDemand", "false"),
("subject", .contains("Future"))
),
playgroundDescription: "Future",
sut: Sut { _ in }
)
}
}
@available(macOS, deprecated: 10.16, message: """
If macOS 10.16/11.0 has already been released, this property should be removed
""")
@available(iOS, deprecated: 14, message: """
If iOS 14 has already been released, this property should be removed
""")
private var leaksSubscription: Bool { // swiftlint:disable:this let_var_whitespace
#if OPENCOMBINE_COMPATIBILITY_TEST
if #available(macOS 10.16, iOS 14.0, *) {
return false
} else {
return true
}
#else
return false
#endif
}
@available(macOS, deprecated: 10.16, message: """
If macOS 10.16/11.0 has already been released, this property should be removed
""")
@available(iOS, deprecated: 14, message: """
If iOS 14 has already been released, this property should be removed
""")
private var hasMissingFailureAfterLateSubscriptionBug: Bool {
// swiftlint:disable:previous let_var_whitespace
#if OPENCOMBINE_COMPATIBILITY_TEST
if #available(macOS 10.16, iOS 14.0, *) {
return false
} else {
return true
}
#else
return false
#endif
}
@@ -113,7 +113,7 @@ final class IgnoreOutputTests: XCTestCase {
.cancelled])
}
func testSendsSubcriptionDownstreamBeforeDemandUpstream() {
func testSendsSubscriptionDownstreamBeforeDemandUpstream() {
var didReceiveSubscription = false
let subscription = CustomSubscription()
let publisher = CustomPublisherBase<Int, Error>(subscription: subscription)
@@ -210,7 +210,7 @@ final class JustTests: XCTestCase {
XCTAssertEqual(Sut("f").first(), Sut("f"))
}
func testFirstWhereOperatorSpecializtion() {
func testFirstWhereOperatorSpecialization() {
XCTAssertEqual(Sut<Int>(42).first { $0 != 42 }, .init(nil))
XCTAssertEqual(Sut<Int>(-13).first { $0 != 42 }, .init(-13))
XCTAssertEqual(Sut<Int>(1).first { $0 < 0 }, .init(nil))
@@ -221,7 +221,7 @@ final class JustTests: XCTestCase {
XCTAssertEqual(Sut("g").last(), Sut("g"))
}
func testLastWhereOperatorSpecializtion() {
func testLastWhereOperatorSpecialization() {
XCTAssertEqual(Sut<Int>(42).last { $0 != 42 }, .init(nil))
XCTAssertEqual(Sut<Int>(-13).last { $0 != 42 }, .init(-13))
XCTAssertEqual(Sut<Int>(1).last { $0 < 0 }, .init(nil))
@@ -26,7 +26,7 @@ final class MapKeyPathTests: XCTestCase {
}
MapTests.testEmpty(valueComparator: ==) {
$0.map(\.doubled, \.tripled, \.quadripled)
$0.map(\.doubled, \.tripled, \.quadrupled)
}
}
@@ -40,7 +40,7 @@ final class MapKeyPathTests: XCTestCase {
}
MapTests.testError(valueComparator: ==) {
$0.map(\.doubled, \.tripled, \.quadripled)
$0.map(\.doubled, \.tripled, \.quadrupled)
}
}
@@ -54,14 +54,14 @@ final class MapKeyPathTests: XCTestCase {
{ $0.map(\.doubled, \.tripled) })
MapTests.testRange(valueComparator: ==,
mapping: { ($0.doubled, $0.tripled, $0.quadripled) },
{ $0.map(\.doubled, \.tripled, \.quadripled) })
mapping: { ($0.doubled, $0.tripled, $0.quadrupled) },
{ $0.map(\.doubled, \.tripled, \.quadrupled) })
}
func testNoDemand() {
MapTests.testNoDemand { $0.map(\.doubled) }
MapTests.testNoDemand { $0.map(\.doubled, \.tripled) }
MapTests.testNoDemand { $0.map(\.doubled, \.tripled, \.quadripled) }
MapTests.testNoDemand { $0.map(\.doubled, \.tripled, \.quadrupled) }
}
func testRequestDemandOnSubscribe() {
@@ -74,14 +74,14 @@ final class MapKeyPathTests: XCTestCase {
}
MapTests.testRequestDemandOnSubscribe {
$0.map(\.doubled, \.tripled, \.quadripled)
$0.map(\.doubled, \.tripled, \.quadrupled)
}
}
func testDemandOnReceive() {
MapTests.testDemandOnReceive { $0.map(\.doubled) }
MapTests.testDemandOnReceive { $0.map(\.doubled, \.tripled) }
MapTests.testDemandOnReceive { $0.map(\.doubled, \.tripled, \.quadripled) }
MapTests.testDemandOnReceive { $0.map(\.doubled, \.tripled, \.quadrupled) }
}
func testCompletion() {
@@ -94,27 +94,27 @@ final class MapKeyPathTests: XCTestCase {
}
MapTests.testCompletion(valueComparator: ==) {
$0.map(\.doubled, \.tripled, \.quadripled)
$0.map(\.doubled, \.tripled, \.quadrupled)
}
}
func testCancel() throws {
try MapTests.testCancel { $0.map(\.doubled) }
try MapTests.testCancel { $0.map(\.doubled, \.tripled) }
try MapTests.testCancel { $0.map(\.doubled, \.tripled, \.quadripled) }
try MapTests.testCancel { $0.map(\.doubled, \.tripled, \.quadrupled) }
}
func testCancelAlreadyCancelled() throws {
try MapTests.testCancelAldreadyCancelled {
try MapTests.testCancelAlreadyCancelled {
$0.map(\.doubled)
}
try MapTests.testCancelAldreadyCancelled {
try MapTests.testCancelAlreadyCancelled {
$0.map(\.doubled, \.tripled)
}
try MapTests.testCancelAldreadyCancelled {
$0.map(\.doubled, \.tripled, \.quadripled)
try MapTests.testCancelAlreadyCancelled {
$0.map(\.doubled, \.tripled, \.quadrupled)
}
}
@@ -150,7 +150,7 @@ final class MapKeyPathTests: XCTestCase {
),
playgroundDescription: "ValueForKeys",
subscriberIsAlsoSubscription: false,
{ $0.map(\.doubled, \.tripled, \.quadripled) })
{ $0.map(\.doubled, \.tripled, \.quadrupled) })
}
func testMapKeyPathReceiveValueBeforeSubscription() {
@@ -169,7 +169,7 @@ final class MapKeyPathTests: XCTestCase {
expected: .history([.value((0, 0, 0))],
demand: .max(42),
comparator: ==),
{ $0.map(\.doubled, \.tripled, \.quadripled) })
{ $0.map(\.doubled, \.tripled, \.quadrupled) })
}
func testMapKeyPathReceiveCompletionBeforeSubscription() {
@@ -188,7 +188,7 @@ final class MapKeyPathTests: XCTestCase {
testReceiveCompletionBeforeSubscription(
inputType: Int.self,
expected: .history([.completion(.finished)], comparator: ==),
{ $0.map(\.doubled, \.tripled, \.quadripled) }
{ $0.map(\.doubled, \.tripled, \.quadrupled) }
)
}
@@ -202,7 +202,7 @@ final class MapKeyPathTests: XCTestCase {
}
try testLifecycle(sendValue: 31, cancellingSubscriptionReleasesSubscriber: true) {
$0.map(\.doubled, \.tripled, \.quadripled)
$0.map(\.doubled, \.tripled, \.quadrupled)
}
}
}
@@ -212,5 +212,5 @@ extension Int {
fileprivate var tripled: Int { return self * 3 }
fileprivate var quadripled: Int { return self * 4 }
fileprivate var quadrupled: Int { return self * 4 }
}
@@ -163,7 +163,7 @@ final class MapTests: XCTestCase {
}
func testMapCancelAlreadyCancelled() throws {
try MapTests.testCancelAldreadyCancelled { $0.map { $0 * 2 } }
try MapTests.testCancelAlreadyCancelled { $0.map { $0 * 2 } }
}
func testTryMapCancelAlreadyCancelled() throws {
@@ -608,7 +608,7 @@ final class MapTests: XCTestCase {
line: line)
}
static func testCancelAldreadyCancelled<Map: Publisher>(
static func testCancelAlreadyCancelled<Map: Publisher>(
file: StaticString = #file,
line: UInt = #line,
_ map: (CustomPublisher) -> Map
@@ -173,7 +173,7 @@ final class OptionalPublisherTests: XCTestCase {
"comparator should not be called for removeDuplicates(by:)")
}
func testAllSatifyOperatorSpecialization() {
func testAllSatisfyOperatorSpecialization() {
var count = 0
let predicate: (Int) -> Bool = { count += 1; return $0 > 0 }
@@ -218,7 +218,7 @@ final class OptionalPublisherTests: XCTestCase {
XCTAssertEqual(Sut<Int>(nil).first(), Sut(nil))
}
func testFirstWhereOperatorSpecializtion() {
func testFirstWhereOperatorSpecialization() {
var count = 0
let predicate: (Int) -> Bool = { count += 1; return $0 == 42 }
@@ -234,7 +234,7 @@ final class OptionalPublisherTests: XCTestCase {
XCTAssertEqual(Sut<Int>(nil).last(), Sut(nil))
}
func testLastWhereOperatorSpecializtion() {
func testLastWhereOperatorSpecialization() {
var count = 0
let predicate: (Int) -> Bool = { count += 1; return $0 == 42 }
@@ -98,7 +98,7 @@ final class ReduceTests: XCTestCase {
{ $0.tryReduce(1, *) })
}
func testTryReduceFailureBecausOfThrow() throws {
func testTryReduceFailureBecauseOfThrow() throws {
func reducer(_ accumulator: Int, _ newValue: Int) throws -> Int {
if newValue == 5 {
@@ -278,7 +278,7 @@ final class ResultPublisherTests: XCTestCase {
XCTAssertEqual(count, 2)
}
func testAllSatifyOperatorSpecialization() {
func testAllSatisfyOperatorSpecialization() {
var count = 0
let predicate: (Int) -> Bool = { count += 1; return $0 > 0 }
XCTAssertEqual(Sut<Int>(0).allSatisfy(predicate).result, .success(false))
@@ -288,7 +288,7 @@ final class ResultPublisherTests: XCTestCase {
XCTAssertEqual(count, 2)
}
func testTryAllSatifyOperatorSpecialization() {
func testTryAllSatisfyOperatorSpecialization() {
var count = 0
let predicate: (Int) -> Bool = { count += 1; return $0 > 0 }
let throwingPredicate: (Int) throws -> Bool = { _ in
@@ -362,7 +362,7 @@ final class SequenceTests: XCTestCase {
)
}
func testFirstWhereOperatorSpecializtion() {
func testFirstWhereOperatorSpecialization() {
XCTAssertEqual(makePublisher(1 ..< 9).first { $0.isMultiple(of: 4) }, .init(4))
XCTAssertEqual(makePublisher(1 ..< 9).first { $0.isMultiple(of: 13) }, .init(nil))
XCTAssertEqual(
@@ -531,7 +531,7 @@ final class SequenceTests: XCTestCase {
XCTAssertEqual(makePublisher(EmptyCollection<Int>()).last(), .init(nil))
}
func testLastWhereOperatorSpecializtion() {
func testLastWhereOperatorSpecialization() {
XCTAssertEqual(makePublisher(1 ..< 9).last { $0.isMultiple(of: 4) }, .init(8))
XCTAssertEqual(makePublisher(1 ..< 9).last { $0.isMultiple(of: 13) }, .init(nil))
XCTAssertEqual(
@@ -540,7 +540,7 @@ final class SequenceTests: XCTestCase {
)
}
func testPrependVariadicOperatorSpezialization() {
func testPrependVariadicOperatorSpecialization() {
let baseCollection = TrackingCollection<Int>([4, 5, 6, 7])
XCTAssertEqual(baseCollection.history, [.initFromSequence])
@@ -613,7 +613,7 @@ final class SequenceTests: XCTestCase {
XCTAssertEqual(newCollection.history, [.initFromSequence, .appendSequence])
}
func testAppendVariadicOperatorSpezialization() {
func testAppendVariadicOperatorSpecialization() {
let baseCollection = TrackingCollection<Int>([1, 2, 3])
XCTAssertEqual(baseCollection.history, [.initFromSequence])
@@ -0,0 +1,395 @@
//
// TimeoutTests.swift
//
//
// Created by Sergej Jaskiewicz on 14.06.2020.
//
import XCTest
#if OPENCOMBINE_COMPATIBILITY_TEST
import Combine
#else
import OpenCombine
#endif
@available(macOS 10.15, iOS 13.0, *)
final class TimeoutTests: XCTestCase {
func testBasicBehaviorWithoutCustomError() {
testBasicBehavior(customError: nil)
}
func testBasicBehaviorWithCustomError() {
testBasicBehavior(customError: "timeout")
}
private func testBasicBehavior(customError: TestingError?) {
let expectedCompletion =
customError.map(Subscribers.Completion.failure) ?? .finished
let scheduler = VirtualTimeScheduler()
let helper = OperatorTestHelper(
publisherType: CustomPublisher.self,
initialDemand: .max(2),
receiveValueDemand: .max(1),
createSut: {
$0.timeout(.nanoseconds(13),
scheduler: scheduler,
options: .nontrivialOptions,
customError: customError.map { e in { e } })
}
)
XCTAssertEqual(helper.tracking.history, [.subscription("Timeout")])
XCTAssertEqual(helper.subscription.history, [.requested(.max(2)),
.requested(.unlimited)])
XCTAssertEqual(scheduler.history,
[.now,
.minimumTolerance,
.scheduleAfterDateWithInterval(.nanoseconds(13),
interval: .nanoseconds(13),
tolerance: .nanoseconds(7),
options: .nontrivialOptions)])
scheduler.rewind(to: .nanoseconds(10))
XCTAssertEqual(helper.publisher.send(1), .unlimited)
XCTAssertEqual(helper.tracking.history, [.subscription("Timeout")])
scheduler.rewind(to: .nanoseconds(11))
XCTAssertEqual(helper.publisher.send(2), .unlimited)
XCTAssertEqual(helper.tracking.history, [.subscription("Timeout"),
.value(1)])
scheduler.rewind(to: .nanoseconds(12))
XCTAssertEqual(helper.tracking.history, [.subscription("Timeout"),
.value(1),
.value(2)])
XCTAssertEqual(scheduler.history,
[.now,
.minimumTolerance,
.scheduleAfterDateWithInterval(.nanoseconds(13),
interval: .nanoseconds(13),
tolerance: .nanoseconds(7),
options: .nontrivialOptions),
.now,
.minimumTolerance,
.scheduleAfterDateWithInterval(.nanoseconds(23),
interval: .nanoseconds(13),
tolerance: .nanoseconds(7),
options: .nontrivialOptions),
.schedule(options: .nontrivialOptions),
.now,
.minimumTolerance,
.scheduleAfterDateWithInterval(.nanoseconds(24),
interval: .nanoseconds(13),
tolerance: .nanoseconds(7),
options: .nontrivialOptions),
.schedule(options: .nontrivialOptions)]
)
scheduler.executeScheduledActions(until: .nanoseconds(200))
XCTAssertEqual(helper.tracking.history, [.subscription("Timeout"),
.value(1),
.value(2),
.completion(expectedCompletion)])
XCTAssertEqual(helper.subscription.history, [.requested(.max(2)),
.requested(.unlimited),
.cancelled])
XCTAssertEqual(scheduler.history,
[.now,
.minimumTolerance,
.scheduleAfterDateWithInterval(.nanoseconds(13),
interval: .nanoseconds(13),
tolerance: .nanoseconds(7),
options: .nontrivialOptions),
.now,
.minimumTolerance,
.scheduleAfterDateWithInterval(.nanoseconds(23),
interval: .nanoseconds(13),
tolerance: .nanoseconds(7),
options: .nontrivialOptions),
.schedule(options: .nontrivialOptions),
.now,
.minimumTolerance,
.scheduleAfterDateWithInterval(.nanoseconds(24),
interval: .nanoseconds(13),
tolerance: .nanoseconds(7),
options: .nontrivialOptions),
.schedule(options: .nontrivialOptions)]
)
scheduler.rewind(to: .nanoseconds(210))
helper.publisher.send(completion: .finished)
scheduler.rewind(to: .nanoseconds(220))
helper.publisher.send(completion: .failure(.oops))
scheduler.executeScheduledActions(until: .nanoseconds(400))
XCTAssertEqual(helper.tracking.history, [.subscription("Timeout"),
.value(1),
.value(2),
.completion(expectedCompletion),
.completion(.finished),
.completion(.failure(.oops))])
XCTAssertEqual(helper.subscription.history, [.requested(.max(2)),
.requested(.unlimited),
.cancelled])
XCTAssertEqual(scheduler.history,
[.now,
.minimumTolerance,
.scheduleAfterDateWithInterval(.nanoseconds(13),
interval: .nanoseconds(13),
tolerance: .nanoseconds(7),
options: .nontrivialOptions),
.now,
.minimumTolerance,
.scheduleAfterDateWithInterval(.nanoseconds(23),
interval: .nanoseconds(13),
tolerance: .nanoseconds(7),
options: .nontrivialOptions),
.schedule(options: .nontrivialOptions),
.now,
.minimumTolerance,
.scheduleAfterDateWithInterval(.nanoseconds(24),
interval: .nanoseconds(13),
tolerance: .nanoseconds(7),
options: .nontrivialOptions),
.schedule(options: .nontrivialOptions),
.schedule(options: .nontrivialOptions),
.schedule(options: .nontrivialOptions)]
)
}
func testRequestWithoutCustomError() throws {
try testRequest(customError: nil)
}
func testRequestWithCustomError() throws {
try testRequest(customError: "timeout")
}
private func testRequest(customError: TestingError?) throws {
let scheduler = VirtualTimeScheduler()
let helper = OperatorTestHelper(
publisherType: CustomPublisher.self,
initialDemand: nil,
receiveValueDemand: .max(1),
createSut: {
$0.timeout(.nanoseconds(13),
scheduler: scheduler,
options: .nontrivialOptions,
customError: customError.map { e in { e } })
}
)
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
let downstreamSubscription = try XCTUnwrap(helper.downstreamSubscription)
downstreamSubscription.request(.none)
downstreamSubscription.request(.max(2))
downstreamSubscription.request(.max(42))
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
.requested(.none),
.requested(.max(2)),
.requested(.max(42))])
scheduler.rewind(to: .nanoseconds(14))
downstreamSubscription.request(.max(13))
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
.requested(.none),
.requested(.max(2)),
.requested(.max(42)),
.cancelled])
}
func testCancellationWithoutCustomError() throws {
try testCancellation(customError: nil)
}
func testCancellationWithCustomError() throws {
try testCancellation(customError: "timeout")
}
private func testCancellation(customError: TestingError?) throws {
let scheduler = VirtualTimeScheduler()
let helper = OperatorTestHelper(
publisherType: CustomPublisher.self,
initialDemand: nil,
receiveValueDemand: .max(1),
createSut: {
$0.timeout(.nanoseconds(13),
scheduler: scheduler,
options: .nontrivialOptions,
customError: customError.map { e in { e } })
}
)
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
let downstreamSubscription = try XCTUnwrap(helper.downstreamSubscription)
downstreamSubscription.cancel()
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
.cancelled])
scheduler.rewind(to: .nanoseconds(14))
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
.cancelled])
}
func testCrashesWithImmediateScheduler() {
let publisher = CustomPublisher(subscription: CustomSubscription())
let tracking = TrackingSubscriber()
let timeout = publisher
.timeout(.nanoseconds(10), scheduler: ImmediateScheduler.shared)
assertCrashes {
timeout.subscribe(tracking)
}
}
func testTimeoutReceiveValueBeforeSubscription() {
let scheduler = VirtualTimeScheduler()
testReceiveValueBeforeSubscription(
value: 42,
expected: .history([], demand: .none),
{ $0.timeout(.nanoseconds(13), scheduler: scheduler) }
)
}
func testTimeoutReceiveCompletionBeforeSubscription() {
let scheduler = VirtualTimeScheduler()
testReceiveCompletionBeforeSubscription(
inputType: Int.self,
expected: .history([]),
{ $0.timeout(.nanoseconds(13), scheduler: scheduler) }
)
}
func testTimeoutRequestBeforeSubscription() {
let scheduler = VirtualTimeScheduler()
testRequestBeforeSubscription(
inputType: Int.self,
shouldCrash: false,
{ $0.timeout(.nanoseconds(13), scheduler: scheduler) }
)
}
func testTimeoutReceiveSubscriptionTwice() throws {
let scheduler = VirtualTimeScheduler()
let helper = OperatorTestHelper(
publisherType: CustomPublisher.self,
initialDemand: nil,
receiveValueDemand: .none,
createSut: { $0.timeout(.nanoseconds(13), scheduler: scheduler) }
)
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 testTimeoutCancelBeforeSubscription() {
let scheduler = VirtualTimeScheduler()
testCancelBeforeSubscription(
inputType: Int.self,
shouldCrash: false,
{ $0.timeout(.nanoseconds(13), scheduler: scheduler) }
)
}
func testSetupTimerWeakCapture() {
let scheduler = VirtualTimeScheduler()
var subscriptionDestroyed = false
do {
let helper = OperatorTestHelper(
publisherType: CustomPublisher.self,
initialDemand: nil,
receiveValueDemand: .none,
createSut: { $0.timeout(.nanoseconds(13), scheduler: scheduler) }
)
helper.tracking.onDeinit = { subscriptionDestroyed = true }
}
XCTAssertTrue(subscriptionDestroyed)
}
func testReceiveValueScheduleStrongCapture() {
let scheduler = VirtualTimeScheduler()
var subscriptionDestroyed = false
do {
let helper = OperatorTestHelper(
publisherType: CustomPublisher.self,
initialDemand: nil,
receiveValueDemand: .none,
createSut: { $0.timeout(.nanoseconds(13), scheduler: scheduler) }
)
helper.tracking.onDeinit = { subscriptionDestroyed = true }
XCTAssertEqual(helper.publisher.send(1), .unlimited)
}
XCTAssertFalse(subscriptionDestroyed)
}
func testReceiveCompletionScheduleStrongCapture() {
let scheduler = VirtualTimeScheduler()
var subscriptionDestroyed = false
do {
let helper = OperatorTestHelper(
publisherType: CustomPublisher.self,
initialDemand: nil,
receiveValueDemand: .none,
createSut: { $0.timeout(.nanoseconds(13), scheduler: scheduler) }
)
helper.tracking.onDeinit = { subscriptionDestroyed = true }
helper.publisher.send(completion: .finished)
}
XCTAssertFalse(subscriptionDestroyed)
}
func testTimeoutReflection() throws {
try testReflection(
parentInput: Double.self,
parentFailure: Error.self,
description: "Timeout",
customMirror: childrenIsEmpty,
playgroundDescription: "Timeout",
{ $0.timeout(.nanoseconds(13), scheduler: VirtualTimeScheduler()) }
)
}
}
@@ -23,7 +23,7 @@ final class CompletionTests: XCTestCase {
let encoder = TrackingEncoder()
let decoder = JSONDecoder()
func testDecodingFinised() throws {
func testDecodingFinished() throws {
let successJSON = #"{"success":true}"#
let illFormedSuccessJSON = #"{"error":{"description":"oops"},"success":true}"#
@@ -43,25 +43,25 @@ final class VirtualTimeSchedulerTests: XCTestCase {
XCTAssertEqual(scheduler.history, [.now,
.minimumTolerance,
.scheduleAfterDate(.nanoseconds(10),
tolerance: 0,
tolerance: .nanoseconds(7),
options: nil),
.schedule(options: nil),
.now,
.minimumTolerance,
.scheduleAfterDate(.nanoseconds(5),
tolerance: 0,
tolerance: .nanoseconds(7),
options: nil),
.schedule(options: nil),
.now,
.now,
.minimumTolerance,
.scheduleAfterDate(.nanoseconds(7),
tolerance: 0,
tolerance: .nanoseconds(7),
options: nil),
.now])
}
func testRepeadedAction() {
func testRepeatedAction() {
let scheduler = VirtualTimeScheduler()
var history = [Int]()
let cancellable = scheduler.schedule(after: scheduler.now + .microseconds(2),
@@ -88,12 +88,12 @@ final class VirtualTimeSchedulerTests: XCTestCase {
.minimumTolerance,
.scheduleAfterDateWithInterval(.microseconds(2),
interval: .milliseconds(40),
tolerance: 0,
tolerance: .nanoseconds(7),
options: nil),
.now,
.minimumTolerance,
.scheduleAfterDate(.milliseconds(300),
tolerance: .nanoseconds(0),
tolerance: .nanoseconds(7),
options: nil),
.now,
.now,
@@ -105,4 +105,45 @@ final class VirtualTimeSchedulerTests: XCTestCase {
.now,
.now])
}
func testRewindForward() {
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.rewind(to: .milliseconds(81))
XCTAssertEqual(history, [2000,
40002000,
80002000])
scheduler.executeScheduledActions()
XCTAssertEqual(history, [2000,
40002000,
80002000,
120002000,
160002000,
200002000,
240002000,
280002000])
scheduler.rewind(to: .milliseconds(0))
scheduler.executeScheduledActions()
XCTAssertEqual(history, [2000,
40002000,
80002000,
120002000,
160002000,
200002000,
240002000,
280002000])
}
}