Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 59183ce0a5 | |||
| b1f676d273 | |||
| b2784a1011 | |||
| d67e77c84d | |||
| d680f09932 | |||
| 30b5dd4c2f | |||
| 621f970998 | |||
| d6b70ad309 | |||
| 918e9131ad | |||
| 7f7f397062 | |||
| 3b1437e46c | |||
| 79899f7742 | |||
| 1496bab272 | |||
| 1ebbdb8ea9 | |||
| f861335dc3 | |||
| 769c3c818f | |||
| 910d21da4c | |||
| 6e20956d6d | |||
| e453879d75 | |||
| 98f6b6b337 | |||
| 74b739d74e | |||
| bcba9a19d4 | |||
| 486e166462 | |||
| c6536cf8d3 | |||
| cf41c25cf7 | |||
| b4557fb311 | |||
| f8e6e66ab4 | |||
| 95b42abce3 | |||
| 899a04bb3f | |||
| 5f9a700689 | |||
| a300fd09d3 | |||
| 5973f86c6e | |||
| 1b5afdba26 | |||
| 51d5d1e71d | |||
| a8bc5cc046 | |||
| 86d6170dc9 | |||
| 171131d768 | |||
| d6b4fb4115 | |||
| 014b82b99d |
+26
-20
@@ -1,16 +1,17 @@
|
||||
version: 2
|
||||
jobs:
|
||||
"Execute tests on macOS 10.15.0 (Xcode 11.2.0, Swift 5.1.2)":
|
||||
"Execute tests on macOS 10.15.0 (Xcode 11.3.0, Swift 5.1.3)":
|
||||
macos:
|
||||
xcode: "11.2.0"
|
||||
xcode: "11.3.0"
|
||||
environment:
|
||||
SWIFT_VERSION: "5.1.2"
|
||||
SWIFT_VERSION: "5.1.3"
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Building and running tests in debug mode with coverage
|
||||
command: |
|
||||
make test-debug \
|
||||
SWIFT_BUILD_FLAGS="-Xswiftc -warnings-as-errors" \
|
||||
SWIFT_TEST_FLAGS="--enable-code-coverage --build-path .build-test-debug"
|
||||
xcrun llvm-cov show \
|
||||
-instr-profile=.build-test-debug/debug/codecov/default.profdata \
|
||||
@@ -20,15 +21,17 @@ jobs:
|
||||
name: Building and running tests in debug mode with TSan
|
||||
command: |
|
||||
make test-debug-sanitize-thread \
|
||||
SWIFT_BUILD_FLAGS="-Xswiftc -warnings-as-errors" \
|
||||
SWIFT_TEST_FLAGS="--build-path .build-test-debug-sanitize-thread"
|
||||
- run:
|
||||
name: Building and running tests in release mode
|
||||
command: |
|
||||
make test-release \
|
||||
SWIFT_BUILD_FLAGS="-Xswiftc -warnings-as-errors" \
|
||||
SWIFT_TEST_FLAGS="--build-path .build-test-release"
|
||||
- run:
|
||||
name: Generating Xcode project
|
||||
command: make generate-xcodeproj
|
||||
command: make generate-xcodeproj SWIFT_BUILD_FLAGS="-Xswiftc -warnings-as-errors"
|
||||
- run:
|
||||
name: Building for testing on macOS 10.15.0 with xcodebuild
|
||||
command: |
|
||||
@@ -60,35 +63,35 @@ jobs:
|
||||
command: |
|
||||
bash <(curl -s https://codecov.io/bash) -D DerivedData
|
||||
|
||||
"Execute compatibility tests on iOS 13.2.2 (Xcode 11.2.0, Swift 5.1.2)":
|
||||
"Execute compatibility tests on iOS 13.4 (Xcode 11.4.0, Swift 5.2.0)":
|
||||
macos:
|
||||
xcode: "11.2.0"
|
||||
xcode: "11.4.0"
|
||||
environment:
|
||||
SWIFT_VERSION: "5.1.2"
|
||||
SWIFT_VERSION: "5.2.0"
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Generating Xcode project
|
||||
command: make generate-compatibility-xcodeproj
|
||||
- run:
|
||||
name: Building for testing on iOS 13.2.2 with xcodebuild
|
||||
name: Building for testing on iOS 13.4 with xcodebuild
|
||||
command: |
|
||||
set -o pipefail \
|
||||
&& xcodebuild build-for-testing \
|
||||
-scheme OpenCombine-Package \
|
||||
-destination "platform=iOS Simulator,name=iPhone 11,OS=13.2.2" \
|
||||
-destination "platform=iOS Simulator,name=iPhone 11,OS=13.4" \
|
||||
-derivedDataPath DerivedData \
|
||||
| tee xcodebuild_build-for-testing.log \
|
||||
| xcpretty
|
||||
- store_artifacts:
|
||||
path: xcodebuild_build-for-testing.log
|
||||
- run:
|
||||
name: Testing against Combine on iOS 13.2.2 with xcodebuild
|
||||
name: Testing against Combine on iOS 13.4 with xcodebuild
|
||||
command: |
|
||||
set -o pipefail \
|
||||
&& xcodebuild test-without-building \
|
||||
-scheme OpenCombine-Package \
|
||||
-destination "platform=iOS Simulator,name=iPhone 11,OS=13.2.2" \
|
||||
-destination "platform=iOS Simulator,name=iPhone 11,OS=13.4" \
|
||||
-derivedDataPath DerivedData \
|
||||
| tee xcodebuild_test-without-building.log \
|
||||
| xcpretty --report junit -o build/reports/results.xml
|
||||
@@ -125,7 +128,7 @@ jobs:
|
||||
- run:
|
||||
name: Generating Xcode project
|
||||
command: |
|
||||
make generate-xcodeproj
|
||||
make generate-xcodeproj SWIFT_BUILD_FLAGS="-Xswiftc -warnings-as-errors"
|
||||
xcodebuild -scheme OpenCombine-Package -showdestinations
|
||||
- run:
|
||||
name: Building for testing on iOS 9.3 with xcodebuild
|
||||
@@ -158,11 +161,11 @@ jobs:
|
||||
command: |
|
||||
bash <(curl -s https://codecov.io/bash) -D DerivedData
|
||||
|
||||
"Execute tests on Ubuntu 18.04 (Swift 5.1.1)":
|
||||
"Execute tests on Ubuntu 18.04 (Swift 5.2)":
|
||||
docker:
|
||||
- image: swift:5.1.1-bionic
|
||||
- image: swift:5.2-bionic
|
||||
environment:
|
||||
SWIFT_VERSION: "5.1.1"
|
||||
SWIFT_VERSION: "5.2"
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
@@ -182,6 +185,7 @@ jobs:
|
||||
> /dev/null 2>&1 \
|
||||
|| true
|
||||
make test-debug \
|
||||
SWIFT_BUILD_FLAGS="-Xswiftc -warnings-as-errors" \
|
||||
SWIFT_TEST_FLAGS="--enable-test-discovery \
|
||||
--enable-index-store \
|
||||
--enable-code-coverage \
|
||||
@@ -194,6 +198,7 @@ jobs:
|
||||
name: Building and running tests in debug mode with TSan
|
||||
command: | # We need to run the test command twice because of https://bugs.swift.org/browse/SR-10783
|
||||
make test-debug-sanitize-thread \
|
||||
SWIFT_BUILD_FLAGS="-Xswiftc -warnings-as-errors" \
|
||||
SWIFT_TEST_FLAGS="--enable-test-discovery \
|
||||
--enable-index-store \
|
||||
--build-path .build-test-debug-sanitize-thread" \
|
||||
@@ -207,6 +212,7 @@ jobs:
|
||||
name: Building and running tests in release mode
|
||||
command: |
|
||||
make test-release \
|
||||
SWIFT_BUILD_FLAGS="-Xswiftc -warnings-as-errors" \
|
||||
SWIFT_TEST_FLAGS="--enable-test-discovery \
|
||||
--enable-index-store \
|
||||
--build-path .build-test-release"
|
||||
@@ -217,7 +223,7 @@ jobs:
|
||||
|
||||
"Run SwiftLint and Danger":
|
||||
macos:
|
||||
xcode: "11.2.0"
|
||||
xcode: "11.3.0"
|
||||
environment:
|
||||
HOMEBREW_NO_AUTO_UPDATE: "1"
|
||||
steps:
|
||||
@@ -236,7 +242,7 @@ jobs:
|
||||
|
||||
"Run Pod spec lint":
|
||||
macos:
|
||||
xcode: "11.2.0"
|
||||
xcode: "11.3.0"
|
||||
environment:
|
||||
HOMEBREW_NO_AUTO_UPDATE: "1"
|
||||
steps:
|
||||
@@ -250,16 +256,16 @@ workflows:
|
||||
version: 2
|
||||
"OpenCombine: execute tests on macOS":
|
||||
jobs:
|
||||
- "Execute tests on macOS 10.15.0 (Xcode 11.2.0, Swift 5.1.2)"
|
||||
- "Execute tests on macOS 10.15.0 (Xcode 11.3.0, Swift 5.1.3)"
|
||||
"OpenCombine: execute compatibility tests":
|
||||
jobs:
|
||||
- "Execute compatibility tests on iOS 13.2.2 (Xcode 11.2.0, Swift 5.1.2)"
|
||||
- "Execute compatibility tests on iOS 13.4 (Xcode 11.4.0, Swift 5.2.0)"
|
||||
"OpenCombine: execute tests on iOS":
|
||||
jobs:
|
||||
- "Execute tests on iOS 9.3 (Xcode 10.2.1, Swift 5.0.1)"
|
||||
"OpenCombine: execute tests on Linux":
|
||||
jobs:
|
||||
- "Execute tests on Ubuntu 18.04 (Swift 5.1.1)"
|
||||
- "Execute tests on Ubuntu 18.04 (Swift 5.2)"
|
||||
"OpenCombine: run SwiftLint and Danger":
|
||||
jobs:
|
||||
- "Run SwiftLint and Danger"
|
||||
|
||||
@@ -23,6 +23,7 @@ disabled_rules:
|
||||
- trailing_comma
|
||||
- type_body_length
|
||||
- opening_brace
|
||||
- untyped_error_in_catch
|
||||
|
||||
opt_in_rules:
|
||||
- array_init
|
||||
@@ -65,6 +66,10 @@ opt_in_rules:
|
||||
- vertical_whitespace_closing_braces
|
||||
- yoda_condition
|
||||
|
||||
implicit_return:
|
||||
included:
|
||||
- closure
|
||||
|
||||
line_length:
|
||||
warning: 90
|
||||
error: 120
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
Pod::Spec.new do |spec|
|
||||
spec.name = "COpenCombineHelpers"
|
||||
spec.version = "0.5.0"
|
||||
spec.summary = "C++ Helpers for OpenCombine"
|
||||
|
||||
spec.description = <<-DESC
|
||||
C++ helpers necessary for the implementation of OpenCombine
|
||||
DESC
|
||||
|
||||
spec.homepage = "https://github.com/broadwaylamb/OpenCombine/"
|
||||
spec.license = "MIT"
|
||||
|
||||
spec.authors = { "Sergej Jaskiewicz" => "jaskiewiczs@icloud.com" }
|
||||
spec.source = { :git => "https://github.com/broadwaylamb/OpenCombine.git", :tag => "#{spec.version}" }
|
||||
|
||||
spec.osx.deployment_target = "10.10"
|
||||
spec.ios.deployment_target = "8.0"
|
||||
spec.watchos.deployment_target = "2.0"
|
||||
spec.tvos.deployment_target = "9.0"
|
||||
|
||||
spec.header_mappings_dir = "Sources/COpenCombineHelpers/include"
|
||||
spec.source_files = "Sources/COpenCombineHelpers/**/*.{cpp,h}"
|
||||
spec.libraries = "c++"
|
||||
|
||||
spec.pod_target_xcconfig = {
|
||||
"DEFINES_MODULE" => "YES"
|
||||
}
|
||||
end
|
||||
+1
-1
@@ -18,7 +18,7 @@ GEM
|
||||
unf (>= 0.0.5, < 1.0.0)
|
||||
dotenv (2.7.5)
|
||||
emoji_regex (1.0.1)
|
||||
excon (0.68.0)
|
||||
excon (0.71.0)
|
||||
faraday (0.17.0)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
faraday-cookie_jar (0.0.6)
|
||||
|
||||
+6
-4
@@ -1,6 +1,6 @@
|
||||
Pod::Spec.new do |spec|
|
||||
spec.name = "OpenCombine"
|
||||
spec.version = "0.5.0"
|
||||
spec.version = "0.9.0"
|
||||
spec.summary = "Open source implementation of Apple's Combine framework for processing values over time."
|
||||
|
||||
spec.description = <<-DESC
|
||||
@@ -20,6 +20,8 @@ Pod::Spec.new do |spec|
|
||||
spec.watchos.deployment_target = "2.0"
|
||||
spec.tvos.deployment_target = "9.0"
|
||||
|
||||
spec.source_files = "Sources/OpenCombine/**/*.swift"
|
||||
spec.dependency "COpenCombineHelpers"
|
||||
end
|
||||
spec.source_files = "Sources/COpenCombineHelpers/**/*.{h,cpp}", "Sources/OpenCombine/**/*.swift"
|
||||
spec.public_header_files = "Sources/COpenCombineHelpers/include/*.h"
|
||||
|
||||
spec.libraries = "c++"
|
||||
end
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
Pod::Spec.new do |spec|
|
||||
spec.name = "OpenCombineDispatch"
|
||||
spec.version = "0.5.0"
|
||||
spec.summary = "OpenCombine Dispatching"
|
||||
spec.version = "0.9.0"
|
||||
spec.summary = "OpenCombine + Dispatch interoperability"
|
||||
|
||||
spec.description = <<-DESC
|
||||
Extends `DispatchQueue` with new methods and nested types.
|
||||
Extends `DispatchQueue` with conformance to the `Scheduler` protocol
|
||||
DESC
|
||||
|
||||
spec.homepage = "https://github.com/broadwaylamb/OpenCombine/"
|
||||
@@ -21,5 +21,5 @@ Pod::Spec.new do |spec|
|
||||
spec.tvos.deployment_target = "9.0"
|
||||
|
||||
spec.source_files = "Sources/OpenCombineDispatch/**/*.swift"
|
||||
spec.dependency "OpenCombine"
|
||||
end
|
||||
spec.dependency "OpenCombine", '>= 0.8'
|
||||
end
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
Pod::Spec.new do |spec|
|
||||
spec.name = "OpenCombineFoundation"
|
||||
spec.version = "0.9.0"
|
||||
spec.summary = "OpenCombine + OpenCombineFoundation interoperability"
|
||||
|
||||
spec.description = <<-DESC
|
||||
Adds publishers to Foundation types like NotificationCenter, URLSession etc.
|
||||
DESC
|
||||
|
||||
spec.homepage = "https://github.com/broadwaylamb/OpenCombine/"
|
||||
spec.license = "MIT"
|
||||
|
||||
spec.authors = { "Sergej Jaskiewicz" => "jaskiewiczs@icloud.com" }
|
||||
spec.source = { :git => "https://github.com/broadwaylamb/OpenCombine.git", :tag => "#{spec.version}" }
|
||||
|
||||
spec.swift_version = "5.0"
|
||||
|
||||
spec.osx.deployment_target = "10.10"
|
||||
spec.ios.deployment_target = "8.0"
|
||||
spec.watchos.deployment_target = "2.0"
|
||||
spec.tvos.deployment_target = "9.0"
|
||||
|
||||
spec.source_files = "Sources/OpenCombineFoundation/**/*.swift"
|
||||
spec.dependency "OpenCombine", '>= 0.8'
|
||||
end
|
||||
+5
-1
@@ -7,14 +7,18 @@ let package = Package(
|
||||
products: [
|
||||
.library(name: "OpenCombine", targets: ["OpenCombine"]),
|
||||
.library(name: "OpenCombineDispatch", targets: ["OpenCombineDispatch"]),
|
||||
.library(name: "OpenCombineFoundation", targets: ["OpenCombineFoundation"]),
|
||||
],
|
||||
targets: [
|
||||
.target(name: "COpenCombineHelpers"),
|
||||
.target(name: "OpenCombine", dependencies: ["COpenCombineHelpers"]),
|
||||
.target(name: "OpenCombineDispatch", dependencies: ["OpenCombine"]),
|
||||
.target(name: "OpenCombineFoundation", dependencies: ["OpenCombine",
|
||||
"COpenCombineHelpers"]),
|
||||
.testTarget(name: "OpenCombineTests",
|
||||
dependencies: ["OpenCombine",
|
||||
"OpenCombineDispatch"],
|
||||
"OpenCombineDispatch",
|
||||
"OpenCombineFoundation"],
|
||||
swiftSettings: [.unsafeFlags(["-enable-testing"])])
|
||||
],
|
||||
cxxLanguageStandard: .cxx1z
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
[](https://codecov.io/gh/broadwaylamb/OpenCombine)
|
||||

|
||||

|
||||

|
||||
[<img src="https://img.shields.io/badge/slack-OpenCombine-yellow.svg?logo=slack">](https://join.slack.com/t/opencombine/shared_invite/enQtNzE2MjE5NzkxODI0LTYxMjkzNDUxZWViZWI1Njc2YjBhODgxNjRjOTdkZTcxOGU2ZjJjZjYxMGI3NWZkN2RkNGFmZTUzNmU3MGE2ZWM)
|
||||
|
||||
Open-source implementation of Apple's [Combine](https://developer.apple.com/documentation/combine) framework for processing values over time.
|
||||
@@ -12,9 +13,9 @@ The main goal of this project is to provide a compatible, reliable and efficient
|
||||
The project is in early development.
|
||||
|
||||
### Installation
|
||||
`OpenCombine` contains two public targets: `OpenCombine` and `OpenCombineDispatch` (the third one, `COpenCombineHelpers`, is considered private. Don't import it in your projects).
|
||||
`OpenCombine` contains three public targets: `OpenCombine`, `OpenCombineFoundation` and `OpenCombineDispatch` (the fourth one, `COpenCombineHelpers`, is considered private. Don't import it in your projects).
|
||||
|
||||
OpenCombine itself does not have any dependencies. Not even Foundation or Dispatch. If you want to use OpenCombine with Dispatch (for example for using `DispatchQueue` as `Scheduler` for operators like `debounce`, `receive(on:)` etc.), you will need to import both `OpenCombine` and `OpenCombineDispatch`.
|
||||
OpenCombine itself does not have any dependencies. Not even Foundation or Dispatch. If you want to use OpenCombine with Dispatch (for example for using `DispatchQueue` as `Scheduler` for operators like `debounce`, `receive(on:)` etc.), you will need to import both `OpenCombine` and `OpenCombineDispatch`. The same applies to Foundation: if you want to use, for instance, `NotificationCenter` or `URLSession` publishers, you'll need to also import `OpenCombineFoundation`
|
||||
|
||||
##### Swift Package Manager
|
||||
###### Swift Package
|
||||
@@ -22,10 +23,12 @@ To add `OpenCombine` to your [SPM](https://swift.org/package-manager/) package,
|
||||
|
||||
```swift
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/broadwaylamb/OpenCombine.git", from: "0.5.0")
|
||||
.package(url: "https://github.com/broadwaylamb/OpenCombine.git", from: "0.9.0")
|
||||
],
|
||||
targets: [
|
||||
.target(name: "MyAwesomePackage", dependencies: ["OpenCombine", "OpenCombineDispatch"])
|
||||
.target(name: "MyAwesomePackage", dependencies: ["OpenCombine",
|
||||
"OpenCombineDispatch",
|
||||
"OpenCombineFoundation"])
|
||||
]
|
||||
```
|
||||
|
||||
@@ -43,8 +46,9 @@ 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.5.0'
|
||||
pod 'OpenCombineDispatch', '~> 0.5.0'
|
||||
pod 'OpenCombine', '~> 0.9'
|
||||
pod 'OpenCombineDispatch', '~> 0.9'
|
||||
pod 'OpenCombineFoundation', '~> 0.9'
|
||||
```
|
||||
|
||||
### Contributing
|
||||
@@ -65,6 +69,28 @@ Or enable the `-DOPENCOMBINE_COMPATIBILITY_TEST` compiler flag in Xcode's build
|
||||
|
||||
> 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.
|
||||
|
||||
#### Releasing a new version
|
||||
|
||||
1. Create a new branch from master and call it `release/<major>.<minor>.<patch>`.
|
||||
1. Replace the usages of the old version in `README.md` with the new version (make sure to check the [Swift Package Manager](#swift-package-manager) and [CocoaPods](#cocoapods) sections).
|
||||
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. 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.
|
||||
1. Switch to the master branch and pull the changes.
|
||||
1. Push the release to CocoaPods trunk. For that, execute the following commands:
|
||||
|
||||
```
|
||||
pod trunk push OpenCombine.podspec --verbose --allow-warnings
|
||||
pod trunk push OpenCombineDispatch.podspec --verbose --allow-warnings
|
||||
pod trunk push OpenCombineFoundation.podspec --verbose --allow-warnings
|
||||
```
|
||||
|
||||
Note that you need to be one of the owners of the pod for that.
|
||||
|
||||
#### GYB
|
||||
|
||||
Some publishers in OpenCombine (like `Publishers.MapKeyPath`, `Publishers.Merge`) exist in several
|
||||
@@ -85,3 +111,16 @@ GYB template files have the `.gyb` extension. Run `make gyb` to generate Swift c
|
||||
templates. The generated files are prefixed with `GENERATED-` and are checked into source control. Those
|
||||
files should never be edited directly. Instead, the `.gyb` template should be edited, and after that the files
|
||||
should be regenerated using `make gyb`.
|
||||
|
||||
#### Debugger Support
|
||||
|
||||
The file `opencombine_lldb.py` defines some `lldb` type summaries for easier debugging. These type summaries improve the way `lldb` and Xcode display some OpenCombine values.
|
||||
|
||||
To use `opencombine_lldb.py`, figure out its full path. Let's say the full path is `~/projects/OpenCombine/opencombine_lldb.py`. Then the following statement to your `~/.lldbinit` file:
|
||||
|
||||
command script import ~/projects/OpenCombine/opencombine_lldb.py
|
||||
|
||||
Currently, `opencombine_lldb.py` defines type summaries for these types:
|
||||
|
||||
- `Subscribers.Demand`
|
||||
- That's all for now.
|
||||
|
||||
@@ -2,173 +2,6 @@
|
||||
// Please remove the corresponding piece from this file if you implement something,
|
||||
// and complement this file as features are added in Apple's Combine
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that receives elements from an upstream publisher on a specific scheduler.
|
||||
public struct SubscribeOn<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 scheduler the publisher should use to receive elements.
|
||||
public let scheduler: Context
|
||||
|
||||
/// Scheduler options that customize the delivery of elements.
|
||||
public let options: Context.SchedulerOptions?
|
||||
|
||||
public init(upstream: Upstream, scheduler: Context, options: Context.SchedulerOptions?)
|
||||
|
||||
/// 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 {
|
||||
|
||||
/// Specifies the scheduler on which to perform subscribe, cancel, and request operations.
|
||||
///
|
||||
/// In contrast with `receive(on:options:)`, which affects downstream messages, `subscribe(on:)` changes the execution context of upstream messages. In the following example, requests to `jsonPublisher` are performed on `backgroundQueue`, but elements received from it are performed on `RunLoop.main`.
|
||||
///
|
||||
/// let ioPerformingPublisher == // Some publisher.
|
||||
/// let uiUpdatingSubscriber == // Some subscriber that updates the UI.
|
||||
///
|
||||
/// ioPerformingPublisher
|
||||
/// .subscribe(on: backgroundQueue)
|
||||
/// .receiveOn(on: RunLoop.main)
|
||||
/// .subscribe(uiUpdatingSubscriber)
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - scheduler: The scheduler on which to receive upstream messages.
|
||||
/// - options: Options that customize the delivery of elements.
|
||||
/// - Returns: A publisher which performs upstream operations on the specified scheduler.
|
||||
public func subscribe<S>(on scheduler: S, options: S.SchedulerOptions? = nil) -> Publishers.SubscribeOn<Self, S> where S : Scheduler
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that measures and emits the time interval between events received from an upstream publisher.
|
||||
public struct MeasureInterval<Upstream, Context> : Publisher where Upstream : Publisher, Context : Scheduler {
|
||||
|
||||
/// The kind of values published by this publisher.
|
||||
public typealias Output = Context.SchedulerTimeType.Stride
|
||||
|
||||
/// 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 scheduler on which to deliver elements.
|
||||
public let scheduler: Context
|
||||
|
||||
public init(upstream: Upstream, scheduler: Context)
|
||||
|
||||
/// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)`
|
||||
///
|
||||
/// - SeeAlso: `subscribe(_:)`
|
||||
/// - Parameters:
|
||||
/// - subscriber: The subscriber to attach to this `Publisher`.
|
||||
/// once attached it can begin to receive values.
|
||||
public func receive<S>(subscriber: S) where S : Subscriber, Upstream.Failure == S.Failure, S.Input == Context.SchedulerTimeType.Stride
|
||||
}
|
||||
}
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Measures and emits the time interval between events received from an upstream publisher.
|
||||
///
|
||||
/// The output type of the returned scheduler is the time interval of the provided scheduler.
|
||||
/// - Parameters:
|
||||
/// - scheduler: The scheduler on which to deliver elements.
|
||||
/// - options: Options that customize the delivery of elements.
|
||||
/// - Returns: A publisher that emits elements representing the time interval between the elements it receives.
|
||||
public func measureInterval<S>(using scheduler: S, options: S.SchedulerOptions? = nil) -> Publishers.MeasureInterval<Self, S> where S : Scheduler
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that raises a debugger signal when a provided closure needs to stop the process in the debugger.
|
||||
///
|
||||
/// When any of the provided closures returns `true`, this publisher raises the `SIGTRAP` signal to stop the process in the debugger.
|
||||
/// Otherwise, this publisher passes through values and completions as-is.
|
||||
public struct Breakpoint<Upstream> : Publisher where Upstream : Publisher {
|
||||
|
||||
/// The kind of values published by this publisher.
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
/// The kind of errors this publisher might publish.
|
||||
///
|
||||
/// Use `Never` if this `Publisher` does not publish errors.
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// A closure that executes when the publisher receives a subscription, and can raise a debugger signal by returning a true Boolean value.
|
||||
public let receiveSubscription: ((Subscription) -> Bool)?
|
||||
|
||||
/// A closure that executes when the publisher receives output from the upstream publisher, and can raise a debugger signal by returning a true Boolean value.
|
||||
public let receiveOutput: ((Upstream.Output) -> Bool)?
|
||||
|
||||
/// A closure that executes when the publisher receives completion, and can raise a debugger signal by returning a true Boolean value.
|
||||
public let receiveCompletion: ((Subscribers.Completion<Upstream.Failure>) -> Bool)?
|
||||
|
||||
/// Creates a breakpoint publisher with the provided upstream publisher and breakpoint-raising closures.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - upstream: The publisher from which this publisher receives elements.
|
||||
/// - receiveSubscription: A closure that executes when the publisher receives a subscription, and can raise a debugger signal by returning a true Boolean value.
|
||||
/// - receiveOutput: A closure that executes when the publisher receives output from the upstream publisher, and can raise a debugger signal by returning a true Boolean value.
|
||||
/// - receiveCompletion: A closure that executes when the publisher receives completion, and can raise a debugger signal by returning a true Boolean value.
|
||||
public init(upstream: Upstream, receiveSubscription: ((Subscription) -> Bool)? = nil, receiveOutput: ((Upstream.Output) -> Bool)? = nil, receiveCompletion: ((Subscribers.Completion<Publishers.Breakpoint<Upstream>.Failure>) -> Bool)? = nil)
|
||||
|
||||
/// 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 {
|
||||
|
||||
/// Raises a debugger signal when a provided closure needs to stop the process in the debugger.
|
||||
///
|
||||
/// When any of the provided closures returns `true`, this publisher raises the `SIGTRAP` signal to stop the process in the debugger.
|
||||
/// Otherwise, this publisher passes through values and completions as-is.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - receiveSubscription: A closure that executes when when the publisher receives a subscription. Return `true` from this closure to raise `SIGTRAP`, or false to continue.
|
||||
/// - receiveOutput: A closure that executes when when the publisher receives a value. Return `true` from this closure to raise `SIGTRAP`, or false to continue.
|
||||
/// - receiveCompletion: A closure that executes when when the publisher receives a completion. Return `true` from this closure to raise `SIGTRAP`, or false to continue.
|
||||
/// - Returns: A publisher that raises a debugger signal when one of the provided closures returns `true`.
|
||||
public func breakpoint(receiveSubscription: ((Subscription) -> Bool)? = nil, receiveOutput: ((Self.Output) -> Bool)? = nil, receiveCompletion: ((Subscribers.Completion<Self.Failure>) -> Bool)? = nil) -> Publishers.Breakpoint<Self>
|
||||
|
||||
/// Raises a debugger signal upon receiving a failure.
|
||||
///
|
||||
/// When the upstream publisher fails with an error, this publisher raises the `SIGTRAP` signal, which stops the process in the debugger.
|
||||
/// Otherwise, this publisher passes through values and completions as-is.
|
||||
/// - Returns: A publisher that raises a debugger signal upon receiving a failure.
|
||||
public func breakpointOnError() -> Publishers.Breakpoint<Self>
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that receives and combines the latest elements from two publishers.
|
||||
@@ -371,47 +204,10 @@ extension Publishers {
|
||||
/// once attached it can begin to receive values.
|
||||
public func receive<S>(subscriber: S) where S : Subscriber, Upstream.Failure == S.Failure, S.Input == [Upstream.Output]
|
||||
}
|
||||
|
||||
/// A publisher that buffers a maximum number of items.
|
||||
public struct CollectByCount<Upstream> : Publisher where Upstream : Publisher {
|
||||
|
||||
/// The kind of values published by this publisher.
|
||||
public typealias Output = [Upstream.Output]
|
||||
|
||||
/// The kind of errors this publisher might publish.
|
||||
///
|
||||
/// Use `Never` if this `Publisher` does not publish errors.
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The maximum number of received elements to buffer before publishing.
|
||||
public let count: Int
|
||||
|
||||
public init(upstream: Upstream, count: Int)
|
||||
|
||||
/// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)`
|
||||
///
|
||||
/// - SeeAlso: `subscribe(_:)`
|
||||
/// - Parameters:
|
||||
/// - subscriber: The subscriber to attach to this `Publisher`.
|
||||
/// once attached it can begin to receive values.
|
||||
public func receive<S>(subscriber: S) where S : Subscriber, Upstream.Failure == S.Failure, S.Input == [Upstream.Output]
|
||||
}
|
||||
}
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Collects up to the specified number of elements, and then emits a single array of the collection.
|
||||
///
|
||||
/// If the upstream publisher finishes before filling the buffer, this publisher sends an array of all the items it has received. This may be fewer than `count` elements.
|
||||
/// If the upstream publisher fails with an error, this publisher forwards the error to the downstream receiver instead of sending its output.
|
||||
/// Note: When this publisher receives a request for `.max(n)` elements, it requests `.max(count * n)` from the upstream publisher.
|
||||
/// - Parameter count: The maximum number of received elements to buffer before publishing.
|
||||
/// - Returns: A publisher that collects up to the specified number of elements, and then publishes them as an array.
|
||||
public func collect(_ count: Int) -> Publishers.CollectByCount<Self>
|
||||
|
||||
/// Collects elements by a given strategy, and emits a single array of the collection.
|
||||
///
|
||||
/// If the upstream publisher finishes before filling the buffer, this publisher sends an array of all the items it has received. This may be fewer than `count` elements.
|
||||
@@ -424,62 +220,6 @@ extension Publisher {
|
||||
public func collect<S>(_ strategy: Publishers.TimeGroupingStrategy<S>, options: S.SchedulerOptions? = nil) -> Publishers.CollectByTime<Self, S> where S : Scheduler
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that delivers elements to its downstream subscriber on a specific scheduler.
|
||||
public struct ReceiveOn<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 scheduler the publisher is to use for element delivery.
|
||||
public let scheduler: Context
|
||||
|
||||
/// Scheduler options that customize the delivery of elements.
|
||||
public let options: Context.SchedulerOptions?
|
||||
|
||||
public init(upstream: Upstream, scheduler: Context, options: Context.SchedulerOptions?)
|
||||
|
||||
/// 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 {
|
||||
|
||||
/// Specifies the scheduler on which to receive elements from the publisher.
|
||||
///
|
||||
/// You use the `receive(on:options:)` operator to receive results on a specific scheduler, such as performing UI work on the main run loop.
|
||||
/// In contrast with `subscribe(on:options:)`, which affects upstream messages, `receive(on:options:)` changes the execution context of downstream messages. In the following example, requests to `jsonPublisher` are performed on `backgroundQueue`, but elements received from it are performed on `RunLoop.main`.
|
||||
///
|
||||
/// let jsonPublisher = MyJSONLoaderPublisher() // Some publisher.
|
||||
/// let labelUpdater = MyLabelUpdateSubscriber() // Some subscriber that updates the UI.
|
||||
///
|
||||
/// jsonPublisher
|
||||
/// .subscribe(on: backgroundQueue)
|
||||
/// .receiveOn(on: RunLoop.main)
|
||||
/// .subscribe(labelUpdater)
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - scheduler: The scheduler the publisher is to use for element delivery.
|
||||
/// - options: Scheduler options that customize the element delivery.
|
||||
/// - Returns: A publisher that delivers elements using the specified scheduler.
|
||||
public func receive<S>(on scheduler: S, options: S.SchedulerOptions? = nil) -> Publishers.ReceiveOn<Self, S> where S : Scheduler
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
public struct PrefixUntilOutput<Upstream, Other> : Publisher where Upstream : Publisher, Other : Publisher {
|
||||
@@ -911,49 +651,6 @@ extension Publisher {
|
||||
public func merge(with other: Self) -> Publishers.MergeMany<Self>
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that “flattens” nested publishers.
|
||||
///
|
||||
/// Given a publisher that publishes Publishers, the `SwitchToLatest` publisher produces a sequence of events from only the most recent one.
|
||||
/// For example, given the type `Publisher<Publisher<Data, NSError>, Never>`, calling `switchToLatest()` will result in the type `Publisher<Data, NSError>`. The downstream subscriber sees a continuous stream of values even though they may be coming from different upstream publishers.
|
||||
public struct SwitchToLatest<P, Upstream> : Publisher where P : Publisher, P == Upstream.Output, Upstream : Publisher, P.Failure == Upstream.Failure {
|
||||
|
||||
/// The kind of values published by this publisher.
|
||||
public typealias Output = P.Output
|
||||
|
||||
/// The kind of errors this publisher might publish.
|
||||
///
|
||||
/// Use `Never` if this `Publisher` does not publish errors.
|
||||
public typealias Failure = P.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// Creates a publisher that “flattens” nested publishers.
|
||||
///
|
||||
/// - Parameter upstream: The publisher from which this publisher receives elements.
|
||||
public init(upstream: Upstream)
|
||||
|
||||
/// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)`
|
||||
///
|
||||
/// - SeeAlso: `subscribe(_:)`
|
||||
/// - Parameters:
|
||||
/// - subscriber: The subscriber to attach to this `Publisher`.
|
||||
/// once attached it can begin to receive values.
|
||||
public func receive<S>(subscriber: S) where S : Subscriber, P.Output == S.Input, Upstream.Failure == S.Failure
|
||||
}
|
||||
}
|
||||
|
||||
extension Publisher where Self.Failure == Self.Output.Failure, Self.Output : Publisher {
|
||||
|
||||
/// Flattens the stream of events from multiple upstream publishers to appear as if they were coming from a single stream of events.
|
||||
///
|
||||
/// This operator switches the inner publisher as new ones arrive but keeps the outer one constant for downstream subscribers.
|
||||
/// For example, given the type `Publisher<Publisher<Data, NSError>, Never>`, calling `switchToLatest()` will result in the type `Publisher<Data, NSError>`. The downstream subscriber sees a continuous stream of values even though they may be coming from different upstream publishers.
|
||||
public func switchToLatest() -> Publishers.SwitchToLatest<Self.Output, Self>
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that attempts to recreate its subscription to a failed upstream publisher.
|
||||
@@ -1063,269 +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 replaces an empty stream with a provided element.
|
||||
public struct ReplaceEmpty<Upstream> : Publisher where Upstream : Publisher {
|
||||
|
||||
/// The kind of values published by this publisher.
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
/// The kind of errors this publisher might publish.
|
||||
///
|
||||
/// Use `Never` if this `Publisher` does not publish errors.
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The element to deliver when the upstream publisher finishes without delivering any elements.
|
||||
public let output: Upstream.Output
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
public init(upstream: Upstream, output: Publishers.ReplaceEmpty<Upstream>.Output)
|
||||
|
||||
/// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)`
|
||||
///
|
||||
/// - SeeAlso: `subscribe(_:)`
|
||||
/// - Parameters:
|
||||
/// - subscriber: The subscriber to attach to this `Publisher`.
|
||||
/// once attached it can begin to receive values.
|
||||
public func receive<S>(subscriber: S) where S : Subscriber, Upstream.Failure == S.Failure, Upstream.Output == S.Input
|
||||
}
|
||||
}
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Replaces an empty stream with the provided element.
|
||||
///
|
||||
/// If the upstream publisher finishes without producing any elements, this publisher emits the provided element, then finishes normally.
|
||||
/// - Parameter output: An element to emit when the upstream publisher finishes without emitting any elements.
|
||||
/// - Returns: A publisher that replaces an empty stream with the provided output element.
|
||||
public func replaceEmpty(with output: Self.Output) -> Publishers.ReplaceEmpty<Self>
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that raises a fatal error upon receiving any failure, and otherwise republishes all received input.
|
||||
///
|
||||
/// Use this function for internal sanity checks that are active during testing but do not impact performance of shipping code.
|
||||
public struct AssertNoFailure<Upstream> : Publisher where Upstream : Publisher {
|
||||
|
||||
/// The kind of values published by this publisher.
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
/// The kind of errors this publisher might publish.
|
||||
///
|
||||
/// Use `Never` if this `Publisher` does not publish errors.
|
||||
public typealias Failure = Never
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The string used at the beginning of the fatal error message.
|
||||
public let prefix: String
|
||||
|
||||
/// The filename used in the error message.
|
||||
public let file: StaticString
|
||||
|
||||
/// The line number used in the error message.
|
||||
public let line: UInt
|
||||
|
||||
public init(upstream: Upstream, prefix: String, file: StaticString, line: UInt)
|
||||
|
||||
/// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)`
|
||||
///
|
||||
/// - SeeAlso: `subscribe(_:)`
|
||||
/// - Parameters:
|
||||
/// - subscriber: The subscriber to attach to this `Publisher`.
|
||||
/// once attached it can begin to receive values.
|
||||
public func receive<S>(subscriber: S) where S : Subscriber, Upstream.Output == S.Input, S.Failure == Publishers.AssertNoFailure<Upstream>.Failure
|
||||
}
|
||||
}
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Raises a fatal error when its upstream publisher fails, and otherwise republishes all received input.
|
||||
///
|
||||
/// Use this function for internal sanity checks that are active during testing but do not impact performance of shipping code.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - prefix: A string used at the beginning of the fatal error message.
|
||||
/// - file: A filename used in the error message. This defaults to `#file`.
|
||||
/// - line: A line number used in the error message. This defaults to `#line`.
|
||||
/// - Returns: A publisher that raises a fatal error when its upstream publisher fails.
|
||||
public func assertNoFailure(_ prefix: String = "", file: StaticString = #file, line: UInt = #line) -> Publishers.AssertNoFailure<Self>
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that ignores elements from the upstream publisher until it receives an element from second publisher.
|
||||
public struct DropUntilOutput<Upstream, Other> : Publisher where Upstream : Publisher, Other : Publisher, Upstream.Failure == Other.Failure {
|
||||
|
||||
/// 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 that this publisher receives elements from.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// A publisher to monitor for its first emitted element.
|
||||
public let other: Other
|
||||
|
||||
/// Creates a publisher that ignores elements from the upstream publisher until it receives an element from another publisher.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - upstream: A publisher to drop elements from while waiting for another publisher to emit elements.
|
||||
/// - other: A publisher to monitor for its first emitted element.
|
||||
public init(upstream: Upstream, other: Other)
|
||||
|
||||
/// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)`
|
||||
///
|
||||
/// - SeeAlso: `subscribe(_:)`
|
||||
/// - Parameters:
|
||||
/// - subscriber: The subscriber to attach to this `Publisher`.
|
||||
/// once attached it can begin to receive values.
|
||||
public func receive<S>(subscriber: S) where S : Subscriber, Upstream.Output == S.Input, Other.Failure == S.Failure
|
||||
}
|
||||
}
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Ignores elements from the upstream publisher until it receives an element from a second publisher.
|
||||
///
|
||||
/// This publisher requests a single value from the upstream publisher, and it ignores (drops) all elements from that publisher until the upstream publisher produces a value. After the `other` publisher produces an element, this publisher cancels its subscription to the `other` publisher, and allows events from the `upstream` publisher to pass through.
|
||||
/// After this publisher receives a subscription from the upstream publisher, it passes through backpressure requests from downstream to the upstream publisher. If the upstream publisher acts on those requests before the other publisher produces an item, this publisher drops the elements it receives from the upstream publisher.
|
||||
///
|
||||
/// - Parameter publisher: A publisher to monitor for its first emitted element.
|
||||
/// - Returns: A publisher that drops elements from the upstream publisher until the `other` publisher produces a value.
|
||||
public func drop<P>(untilOutputFrom publisher: P) -> Publishers.DropUntilOutput<Self, P> where P : Publisher, Self.Failure == P.Failure
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that performs the specified closures when publisher events occur.
|
||||
public struct HandleEvents<Upstream> : Publisher where Upstream : Publisher {
|
||||
|
||||
/// The kind of values published by this publisher.
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
/// The kind of errors this publisher might publish.
|
||||
///
|
||||
/// Use `Never` if this `Publisher` does not publish errors.
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// A closure that executes when the publisher receives the subscription from the upstream publisher.
|
||||
public var receiveSubscription: ((Subscription) -> Void)?
|
||||
|
||||
/// A closure that executes when the publisher receives a value from the upstream publisher.
|
||||
public var receiveOutput: ((Upstream.Output) -> Void)?
|
||||
|
||||
/// A closure that executes when the publisher receives the completion from the upstream publisher.
|
||||
public var receiveCompletion: ((Subscribers.Completion<Upstream.Failure>) -> Void)?
|
||||
|
||||
/// A closure that executes when the downstream receiver cancels publishing.
|
||||
public var receiveCancel: (() -> Void)?
|
||||
|
||||
/// A closure that executes when the publisher receives a request for more elements.
|
||||
public var receiveRequest: ((Subscribers.Demand) -> Void)?
|
||||
|
||||
public init(upstream: Upstream, receiveSubscription: ((Subscription) -> Void)? = nil, receiveOutput: ((Publishers.HandleEvents<Upstream>.Output) -> Void)? = nil, receiveCompletion: ((Subscribers.Completion<Publishers.HandleEvents<Upstream>.Failure>) -> Void)? = nil, receiveCancel: (() -> Void)? = nil, receiveRequest: ((Subscribers.Demand) -> Void)?)
|
||||
|
||||
/// 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 {
|
||||
|
||||
/// Performs the specified closures when publisher events occur.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - receiveSubscription: A closure that executes when the publisher receives the subscription from the upstream publisher. Defaults to `nil`.
|
||||
/// - receiveOutput: A closure that executes when the publisher receives a value from the upstream publisher. Defaults to `nil`.
|
||||
/// - receiveCompletion: A closure that executes when the publisher receives the completion from the upstream publisher. Defaults to `nil`.
|
||||
/// - receiveCancel: A closure that executes when the downstream receiver cancels publishing. Defaults to `nil`.
|
||||
/// - receiveRequest: A closure that executes when the publisher receives a request for more elements. Defaults to `nil`.
|
||||
/// - Returns: A publisher that performs the specified closures when publisher events occur.
|
||||
public func handleEvents(receiveSubscription: ((Subscription) -> Void)? = nil, receiveOutput: ((Self.Output) -> Void)? = nil, receiveCompletion: ((Subscribers.Completion<Self.Failure>) -> Void)? = nil, receiveCancel: (() -> Void)? = nil, receiveRequest: ((Subscribers.Demand) -> Void)? = nil) -> Publishers.HandleEvents<Self>
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that emits all of one publisher’s elements before those from another publisher.
|
||||
public struct Concatenate<Prefix, Suffix> : Publisher where Prefix : Publisher, Suffix : Publisher, Prefix.Failure == Suffix.Failure, Prefix.Output == Suffix.Output {
|
||||
|
||||
/// The kind of values published by this publisher.
|
||||
public typealias Output = Suffix.Output
|
||||
|
||||
/// The kind of errors this publisher might publish.
|
||||
///
|
||||
/// Use `Never` if this `Publisher` does not publish errors.
|
||||
public typealias Failure = Suffix.Failure
|
||||
|
||||
/// The publisher to republish, in its entirety, before republishing elements from `suffix`.
|
||||
public let prefix: Prefix
|
||||
|
||||
/// The publisher to republish only after `prefix` finishes.
|
||||
public let suffix: Suffix
|
||||
|
||||
public init(prefix: Prefix, suffix: Suffix)
|
||||
|
||||
/// 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, Suffix.Failure == S.Failure, Suffix.Output == S.Input
|
||||
}
|
||||
}
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Prefixes a `Publisher`'s output with the specified sequence.
|
||||
/// - Parameter elements: The elements to publish before this publisher’s elements.
|
||||
/// - Returns: A publisher that prefixes the specified elements prior to this publisher’s elements.
|
||||
public func prepend(_ elements: Self.Output...) -> Publishers.Concatenate<Publishers.Sequence<[Self.Output], Self.Failure>, Self>
|
||||
|
||||
/// Prefixes a `Publisher`'s output with the specified sequence.
|
||||
/// - Parameter elements: A sequence of elements to publish before this publisher’s elements.
|
||||
/// - Returns: A publisher that prefixes the sequence of elements prior to this publisher’s elements.
|
||||
public func prepend<S>(_ elements: S) -> Publishers.Concatenate<Publishers.Sequence<S, Self.Failure>, Self> where S : Sequence, Self.Output == S.Element
|
||||
|
||||
/// Prefixes this publisher’s output with the elements emitted by the given publisher.
|
||||
///
|
||||
/// The resulting publisher doesn’t emit any elements until the prefixing publisher finishes.
|
||||
/// - Parameter publisher: The prefixing publisher.
|
||||
/// - Returns: A publisher that prefixes the prefixing publisher’s elements prior to this publisher’s elements.
|
||||
public func prepend<P>(_ publisher: P) -> Publishers.Concatenate<P, Self> where P : Publisher, Self.Failure == P.Failure, Self.Output == P.Output
|
||||
|
||||
/// Append a `Publisher`'s output with the specified sequence.
|
||||
public func append(_ elements: Self.Output...) -> Publishers.Concatenate<Self, Publishers.Sequence<[Self.Output], Self.Failure>>
|
||||
|
||||
/// Appends a `Publisher`'s output with the specified sequence.
|
||||
public func append<S>(_ elements: S) -> Publishers.Concatenate<Self, Publishers.Sequence<S, Self.Failure>> where S : Sequence, Self.Output == S.Element
|
||||
|
||||
/// Appends this publisher’s output with the elements emitted by the given publisher.
|
||||
///
|
||||
/// This operator produces no elements until this publisher finishes. It then produces this publisher’s elements, followed by the given publisher’s elements. If this publisher fails with an error, the prefixing publisher does not publish the provided publisher’s elements.
|
||||
/// - Parameter publisher: The appending publisher.
|
||||
/// - Returns: A publisher that appends the appending publisher’s elements after this publisher’s elements.
|
||||
public func append<P>(_ publisher: P) -> Publishers.Concatenate<Self, P> where P : Publisher, Self.Failure == P.Failure, Self.Output == P.Output
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that publishes elements only after a specified time interval elapses between events.
|
||||
@@ -1423,122 +857,6 @@ extension 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 strategy for filling a buffer.
|
||||
///
|
||||
/// * keepFull: A strategy to fill the buffer at subscription time, and keep it full thereafter.
|
||||
/// * byRequest: A strategy that avoids prefetching and instead performs requests on demand.
|
||||
public enum PrefetchStrategy {
|
||||
|
||||
case keepFull
|
||||
|
||||
case byRequest
|
||||
|
||||
/// Returns a Boolean value indicating whether two values are equal.
|
||||
///
|
||||
/// Equality is the inverse of inequality. For any values `a` and `b`,
|
||||
/// `a == b` implies that `a != b` is `false`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - lhs: A value to compare.
|
||||
/// - rhs: Another value to compare.
|
||||
public static func == (a: Publishers.PrefetchStrategy, b: Publishers.PrefetchStrategy) -> Bool
|
||||
|
||||
/// The hash value.
|
||||
///
|
||||
/// Hash values are not guaranteed to be equal across different executions of
|
||||
/// your program. Do not save hash values to use during a future execution.
|
||||
///
|
||||
/// - Important: `hashValue` is deprecated as a `Hashable` requirement. To
|
||||
/// conform to `Hashable`, implement the `hash(into:)` requirement instead.
|
||||
public var hashValue: Int { get }
|
||||
|
||||
/// Hashes the essential components of this value by feeding them into the
|
||||
/// given hasher.
|
||||
///
|
||||
/// Implement this method to conform to the `Hashable` protocol. The
|
||||
/// components used for hashing must be the same as the components compared
|
||||
/// in your type's `==` operator implementation. Call `hasher.combine(_:)`
|
||||
/// with each of these components.
|
||||
///
|
||||
/// - Important: Never call `finalize()` on `hasher`. Doing so may become a
|
||||
/// compile-time error in the future.
|
||||
///
|
||||
/// - Parameter hasher: The hasher to use when combining the components
|
||||
/// of this instance.
|
||||
public func hash(into hasher: inout Hasher)
|
||||
}
|
||||
|
||||
/// A strategy for handling exhaustion of a buffer’s capacity.
|
||||
///
|
||||
/// * dropNewest: When full, discard the newly-received element without buffering it.
|
||||
/// * dropOldest: When full, remove the least recently-received element from the buffer.
|
||||
/// * customError: When full, execute the closure to provide a custom error.
|
||||
public enum BufferingStrategy<Failure> where Failure : Error {
|
||||
|
||||
case dropNewest
|
||||
|
||||
case dropOldest
|
||||
|
||||
case customError(() -> Failure)
|
||||
}
|
||||
|
||||
/// A publisher that buffers elements received from an upstream publisher.
|
||||
public struct Buffer<Upstream> : Publisher where Upstream : Publisher {
|
||||
|
||||
/// The kind of values published by this publisher.
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
/// The kind of errors this publisher might publish.
|
||||
///
|
||||
/// Use `Never` if this `Publisher` does not publish errors.
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The maximum number of elements to store.
|
||||
public let size: Int
|
||||
|
||||
/// The strategy for initially populating the buffer.
|
||||
public let prefetch: Publishers.PrefetchStrategy
|
||||
|
||||
/// The action to take when the buffer becomes full.
|
||||
public let whenFull: Publishers.BufferingStrategy<Upstream.Failure>
|
||||
|
||||
/// Creates a publisher that buffers elements received from an upstream publisher.
|
||||
/// - Parameter upstream: The publisher from which this publisher receives elements.
|
||||
/// - Parameter size: The maximum number of elements to store.
|
||||
/// - Parameter prefetch: The strategy for initially populating the buffer.
|
||||
/// - Parameter whenFull: The action to take when the buffer becomes full.
|
||||
public init(upstream: Upstream, size: Int, prefetch: Publishers.PrefetchStrategy, whenFull: Publishers.BufferingStrategy<Publishers.Buffer<Upstream>.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 Publishers.PrefetchStrategy : Equatable {
|
||||
}
|
||||
|
||||
extension Publishers.PrefetchStrategy : Hashable {
|
||||
}
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Buffers elements received from an upstream publisher.
|
||||
/// - Parameter size: The maximum number of elements to store.
|
||||
/// - Parameter prefetch: The strategy for initially populating the buffer.
|
||||
/// - Parameter whenFull: The action to take when the buffer becomes full.
|
||||
public func buffer(size: Int, prefetch: Publishers.PrefetchStrategy, whenFull: Publishers.BufferingStrategy<Self.Failure>) -> Publishers.Buffer<Self>
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher created by applying the zip function to two upstream publishers.
|
||||
@@ -1702,162 +1020,6 @@ extension Publisher {
|
||||
public func zip<P, Q, R, T>(_ publisher1: P, _ publisher2: Q, _ publisher3: R, _ transform: @escaping (Self.Output, P.Output, Q.Output, R.Output) -> T) -> Publishers.Map<Publishers.Zip4<Self, P, Q, R>, T> where P : Publisher, Q : Publisher, R : Publisher, Self.Failure == P.Failure, P.Failure == Q.Failure, Q.Failure == R.Failure
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that handles errors from an upstream publisher by replacing the failed publisher with another publisher.
|
||||
public struct Catch<Upstream, NewPublisher> : Publisher where Upstream : Publisher, NewPublisher : Publisher, Upstream.Output == NewPublisher.Output {
|
||||
|
||||
/// 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 = NewPublisher.Failure
|
||||
|
||||
/// The publisher that this publisher receives elements from.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// A closure that accepts the upstream failure as input and returns a publisher to replace the upstream publisher.
|
||||
public let handler: (Upstream.Failure) -> NewPublisher
|
||||
|
||||
/// Creates a publisher that handles errors from an upstream publisher by replacing the failed publisher with another publisher.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - upstream: The publisher that this publisher receives elements from.
|
||||
/// - handler: A closure that accepts the upstream failure as input and returns a publisher to replace the upstream publisher.
|
||||
public init(upstream: Upstream, handler: @escaping (Upstream.Failure) -> NewPublisher)
|
||||
|
||||
/// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)`
|
||||
///
|
||||
/// - SeeAlso: `subscribe(_:)`
|
||||
/// - Parameters:
|
||||
/// - subscriber: The subscriber to attach to this `Publisher`.
|
||||
/// once attached it can begin to receive values.
|
||||
public func receive<S>(subscriber: S) where S : Subscriber, NewPublisher.Failure == S.Failure, NewPublisher.Output == S.Input
|
||||
}
|
||||
|
||||
/// A publisher that handles errors from an upstream publisher by replacing the failed publisher with another publisher or optionally producing a new error.
|
||||
public struct TryCatch<Upstream, NewPublisher> : Publisher where Upstream : Publisher, NewPublisher : Publisher, Upstream.Output == NewPublisher.Output {
|
||||
|
||||
/// The kind of values published by this publisher.
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
/// The kind of errors this publisher might publish.
|
||||
///
|
||||
/// Use `Never` if this `Publisher` does not publish errors.
|
||||
public typealias Failure = Error
|
||||
|
||||
public let upstream: Upstream
|
||||
|
||||
public let handler: (Upstream.Failure) throws -> NewPublisher
|
||||
|
||||
public init(upstream: Upstream, handler: @escaping (Upstream.Failure) throws -> NewPublisher)
|
||||
|
||||
/// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)`
|
||||
///
|
||||
/// - SeeAlso: `subscribe(_:)`
|
||||
/// - Parameters:
|
||||
/// - subscriber: The subscriber to attach to this `Publisher`.
|
||||
/// once attached it can begin to receive values.
|
||||
public func receive<S>(subscriber: S) where S : Subscriber, NewPublisher.Output == S.Input, S.Failure == Publishers.TryCatch<Upstream, NewPublisher>.Failure
|
||||
}
|
||||
}
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Handles errors from an upstream publisher by replacing it with another publisher.
|
||||
///
|
||||
/// The following example replaces any error from the upstream publisher and replaces the upstream with a `Just` publisher. This continues the stream by publishing a single value and completing normally.
|
||||
/// ```
|
||||
/// enum SimpleError: Error { case error }
|
||||
/// let errorPublisher = (0..<10).publisher.tryMap { v -> Int in
|
||||
/// if v < 5 {
|
||||
/// return v
|
||||
/// } else {
|
||||
/// throw SimpleError.error
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// let noErrorPublisher = errorPublisher.catch { _ in
|
||||
/// return Just(100)
|
||||
/// }
|
||||
/// ```
|
||||
/// Backpressure note: This publisher passes through `request` and `cancel` to the upstream. After receiving an error, the publisher sends sends any unfulfilled demand to the new `Publisher`.
|
||||
/// - Parameter handler: A closure that accepts the upstream failure as input and returns a publisher to replace the upstream publisher.
|
||||
/// - Returns: A publisher that handles errors from an upstream publisher by replacing the failed publisher with another publisher.
|
||||
public func `catch`<P>(_ handler: @escaping (Self.Failure) -> P) -> Publishers.Catch<Self, P> where P : Publisher, Self.Output == P.Output
|
||||
|
||||
/// Handles errors from an upstream publisher by either replacing it with another publisher or `throw`ing a new error.
|
||||
///
|
||||
/// - Parameter handler: A `throw`ing closure that accepts the upstream failure as input and returns a publisher to replace the upstream publisher or if an error is thrown will send the error downstream.
|
||||
/// - Returns: A publisher that handles errors from an upstream publisher by replacing the failed publisher with another publisher.
|
||||
public func tryCatch<P>(_ handler: @escaping (Self.Failure) throws -> P) -> Publishers.TryCatch<Self, P> where P : Publisher, Self.Output == P.Output
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that delays delivery of elements and completion to the downstream receiver.
|
||||
public struct Delay<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 that this publisher receives elements from.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The amount of time to delay.
|
||||
public let interval: Context.SchedulerTimeType.Stride
|
||||
|
||||
/// The allowed tolerance in firing delayed events.
|
||||
public let tolerance: Context.SchedulerTimeType.Stride
|
||||
|
||||
/// The scheduler to deliver the delayed events.
|
||||
public let scheduler: Context
|
||||
|
||||
public let options: Context.SchedulerOptions?
|
||||
|
||||
public init(upstream: Upstream, interval: Context.SchedulerTimeType.Stride, tolerance: Context.SchedulerTimeType.Stride, scheduler: Context, options: Context.SchedulerOptions? = nil)
|
||||
|
||||
/// 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 {
|
||||
|
||||
/// Delays delivery of all output to the downstream receiver by a specified amount of time on a particular scheduler.
|
||||
///
|
||||
/// The delay affects the delivery of elements and completion, but not of the original subscription.
|
||||
/// - Parameters:
|
||||
/// - interval: The amount of time to delay.
|
||||
/// - tolerance: The allowed tolerance in firing delayed events.
|
||||
/// - scheduler: The scheduler to deliver the delayed events.
|
||||
/// - Returns: A publisher that delays delivery of elements and completion to the downstream receiver.
|
||||
public func delay<S>(for interval: S.SchedulerTimeType.Stride, tolerance: S.SchedulerTimeType.Stride? = nil, scheduler: S, options: S.SchedulerOptions? = nil) -> Publishers.Delay<Self, S> where S : Scheduler
|
||||
}
|
||||
|
||||
extension Just {
|
||||
|
||||
public func prepend(_ elements: Output...) -> Publishers.Sequence<[Output], Just<Output>.Failure>
|
||||
|
||||
public func prepend<S>(_ elements: S) -> Publishers.Sequence<[Output], Just<Output>.Failure> where Output == S.Element, S : Sequence
|
||||
|
||||
public func append(_ elements: Output...) -> Publishers.Sequence<[Output], Just<Output>.Failure>
|
||||
|
||||
public func append<S>(_ elements: S) -> Publishers.Sequence<[Output], Just<Output>.Failure> where Output == S.Element, S : Sequence
|
||||
}
|
||||
|
||||
extension Publishers.CombineLatest : Equatable where A : Equatable, B : Equatable {
|
||||
|
||||
/// Returns a Boolean value that indicates whether two publishers are equivalent.
|
||||
@@ -1895,19 +1057,6 @@ extension Publishers.CombineLatest4 : Equatable where A : Equatable, B : Equatab
|
||||
public static func == (lhs: Publishers.CombineLatest4<A, B, C, D>, rhs: Publishers.CombineLatest4<A, B, C, D>) -> Bool
|
||||
}
|
||||
|
||||
extension Publishers.CollectByCount : Equatable where Upstream : Equatable {
|
||||
|
||||
/// Returns a Boolean value indicating whether two values are equal.
|
||||
///
|
||||
/// Equality is the inverse of inequality. For any values `a` and `b`,
|
||||
/// `a == b` implies that `a != b` is `false`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - lhs: A value to compare.
|
||||
/// - rhs: Another value to compare.
|
||||
public static func == (lhs: Publishers.CollectByCount<Upstream>, rhs: Publishers.CollectByCount<Upstream>) -> Bool
|
||||
}
|
||||
|
||||
extension Publishers.Merge : Equatable where A : Equatable, B : Equatable {
|
||||
|
||||
/// Returns a Boolean value that indicates whether two publishers are equivalent.
|
||||
@@ -2011,41 +1160,6 @@ extension Publishers.Retry : Equatable where Upstream : Equatable {
|
||||
public static func == (lhs: Publishers.Retry<Upstream>, rhs: Publishers.Retry<Upstream>) -> Bool
|
||||
}
|
||||
|
||||
extension Publishers.ReplaceEmpty : Equatable where Upstream : Equatable, Upstream.Output : Equatable {
|
||||
|
||||
/// Returns a Boolean value that indicates whether two publishers are equivalent.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - lhs: A replace empty publisher to compare for equality.
|
||||
/// - rhs: Another replace empty publisher to compare for equality.
|
||||
/// - Returns: `true` if the two publishers have equal upstream publishers and output elements, `false` otherwise.
|
||||
public static func == (lhs: Publishers.ReplaceEmpty<Upstream>, rhs: Publishers.ReplaceEmpty<Upstream>) -> Bool
|
||||
}
|
||||
|
||||
extension Publishers.DropUntilOutput : Equatable where Upstream : Equatable, Other : Equatable {
|
||||
|
||||
/// Returns a Boolean value indicating whether two values are equal.
|
||||
///
|
||||
/// Equality is the inverse of inequality. For any values `a` and `b`,
|
||||
/// `a == b` implies that `a != b` is `false`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - lhs: A value to compare.
|
||||
/// - rhs: Another value to compare.
|
||||
public static func == (lhs: Publishers.DropUntilOutput<Upstream, Other>, rhs: Publishers.DropUntilOutput<Upstream, Other>) -> Bool
|
||||
}
|
||||
|
||||
extension Publishers.Concatenate : Equatable where Prefix : Equatable, Suffix : Equatable {
|
||||
|
||||
/// Returns a Boolean value that indicates whether two publishers are equivalent.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - lhs: A concatenate publisher to compare for equality.
|
||||
/// - rhs: Another concatenate publisher to compare for equality.
|
||||
/// - Returns: `true` if the two publishers’ prefix and suffix properties are equal, `false` otherwise.
|
||||
public static func == (lhs: Publishers.Concatenate<Prefix, Suffix>, rhs: Publishers.Concatenate<Prefix, Suffix>) -> Bool
|
||||
}
|
||||
|
||||
extension Publishers.Zip : Equatable where A : Equatable, B : Equatable {
|
||||
|
||||
/// Returns a Boolean value that indicates whether two publishers are equivalent.
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
// From /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/lib/swift/Foundation.swiftmodule/x86_64.swiftinterface
|
||||
// swift-interface-format-version: 1.0
|
||||
// swift-compiler-version: Apple Swift version 5.1.1 (swiftlang-1100.8.275.1 clang-1100.0.32.1)
|
||||
// swift-module-flags: -target x86_64-apple-macosx10.15 -enable-objc-interop -autolink-force-load -enable-library-evolution -module-link-name swiftFoundation -swift-version 5 -O -enforce-exclusivity=unchecked -module-name Foundation
|
||||
|
||||
public typealias Published = Combine.Published
|
||||
|
||||
public typealias ObservableObject = Combine.ObservableObject
|
||||
|
||||
public protocol _KeyValueCodingAndObservingPublishing {
|
||||
}
|
||||
|
||||
extension NSObject : Foundation._KeyValueCodingAndObservingPublishing {
|
||||
}
|
||||
|
||||
extension _KeyValueCodingAndObservingPublishing where Self : ObjectiveC.NSObject {
|
||||
public func publisher<Value>(for keyPath: Swift.KeyPath<Self, Value>, options: Foundation.NSKeyValueObservingOptions = [.initial, .new]) -> ObjectiveC.NSObject.KeyValueObservingPublisher<Self, Value>
|
||||
}
|
||||
|
||||
extension NSObject.KeyValueObservingPublisher {
|
||||
public func didChange() -> Combine.Publishers.Map<ObjectiveC.NSObject.KeyValueObservingPublisher<Subject, Value>, Swift.Void>
|
||||
}
|
||||
|
||||
extension NSObject {
|
||||
public struct KeyValueObservingPublisher<Subject, Value> : Swift.Equatable where Subject : ObjectiveC.NSObject {
|
||||
public let object: Subject
|
||||
public let keyPath: Swift.KeyPath<Subject, Value>
|
||||
public let options: Foundation.NSKeyValueObservingOptions
|
||||
public init(object: Subject, keyPath: Swift.KeyPath<Subject, Value>, options: Foundation.NSKeyValueObservingOptions)
|
||||
public static func == (lhs: ObjectiveC.NSObject.KeyValueObservingPublisher<Subject, Value>, rhs: ObjectiveC.NSObject.KeyValueObservingPublisher<Subject, Value>) -> Swift.Bool
|
||||
}
|
||||
}
|
||||
|
||||
extension NSObject.KeyValueObservingPublisher : Combine.Publisher {
|
||||
public typealias Output = Value
|
||||
public typealias Failure = Swift.Never
|
||||
public func receive<S>(subscriber: S) where Value == S.Input, S : Combine.Subscriber, S.Failure == ObjectiveC.NSObject.KeyValueObservingPublisher<Subject, Value>.Failure
|
||||
}
|
||||
|
||||
extension Timer {
|
||||
public static func publish(every interval: Foundation.TimeInterval, tolerance: Foundation.TimeInterval? = nil, on runLoop: Foundation.RunLoop, in mode: Foundation.RunLoop.Mode, options: Foundation.RunLoop.SchedulerOptions? = nil) -> Foundation.Timer.TimerPublisher
|
||||
final public class TimerPublisher : Combine.ConnectablePublisher {
|
||||
public typealias Output = Foundation.Date
|
||||
public typealias Failure = Swift.Never
|
||||
final public let interval: Foundation.TimeInterval
|
||||
final public let tolerance: Foundation.TimeInterval?
|
||||
final public let runLoop: Foundation.RunLoop
|
||||
final public let mode: Foundation.RunLoop.Mode
|
||||
final public let options: Foundation.RunLoop.SchedulerOptions?
|
||||
public init(interval: Foundation.TimeInterval, tolerance: Foundation.TimeInterval? = nil, runLoop: Foundation.RunLoop, mode: Foundation.RunLoop.Mode, options: Foundation.RunLoop.SchedulerOptions? = nil)
|
||||
final public func receive<S>(subscriber: S) where S : Combine.Subscriber, S.Failure == Foundation.Timer.TimerPublisher.Failure, S.Input == Foundation.Timer.TimerPublisher.Output
|
||||
final public func connect() -> Combine.Cancellable
|
||||
@objc deinit
|
||||
}
|
||||
}
|
||||
|
||||
extension OperationQueue : Combine.Scheduler {
|
||||
public struct SchedulerTimeType : Swift.Strideable, Swift.Codable, Swift.Hashable {
|
||||
public var date: Foundation.Date
|
||||
public init(_ date: Foundation.Date)
|
||||
public func distance(to other: Foundation.OperationQueue.SchedulerTimeType) -> Foundation.OperationQueue.SchedulerTimeType.Stride
|
||||
public func advanced(by n: Foundation.OperationQueue.SchedulerTimeType.Stride) -> Foundation.OperationQueue.SchedulerTimeType
|
||||
public struct Stride : Swift.ExpressibleByFloatLiteral, Swift.Comparable, Swift.SignedNumeric, Swift.Codable, Combine.SchedulerTimeIntervalConvertible {
|
||||
public typealias FloatLiteralType = Foundation.TimeInterval
|
||||
public typealias IntegerLiteralType = Foundation.TimeInterval
|
||||
public typealias Magnitude = Foundation.TimeInterval
|
||||
public var magnitude: Foundation.TimeInterval
|
||||
public var timeInterval: Foundation.TimeInterval {
|
||||
get
|
||||
}
|
||||
public init(integerLiteral value: Foundation.TimeInterval)
|
||||
public init(floatLiteral value: Foundation.TimeInterval)
|
||||
public init(_ timeInterval: Foundation.TimeInterval)
|
||||
public init?<T>(exactly source: T) where T : Swift.BinaryInteger
|
||||
public static func < (lhs: Foundation.OperationQueue.SchedulerTimeType.Stride, rhs: Foundation.OperationQueue.SchedulerTimeType.Stride) -> Swift.Bool
|
||||
public static func * (lhs: Foundation.OperationQueue.SchedulerTimeType.Stride, rhs: Foundation.OperationQueue.SchedulerTimeType.Stride) -> Foundation.OperationQueue.SchedulerTimeType.Stride
|
||||
public static func + (lhs: Foundation.OperationQueue.SchedulerTimeType.Stride, rhs: Foundation.OperationQueue.SchedulerTimeType.Stride) -> Foundation.OperationQueue.SchedulerTimeType.Stride
|
||||
public static func - (lhs: Foundation.OperationQueue.SchedulerTimeType.Stride, rhs: Foundation.OperationQueue.SchedulerTimeType.Stride) -> Foundation.OperationQueue.SchedulerTimeType.Stride
|
||||
public static func *= (lhs: inout Foundation.OperationQueue.SchedulerTimeType.Stride, rhs: Foundation.OperationQueue.SchedulerTimeType.Stride)
|
||||
public static func += (lhs: inout Foundation.OperationQueue.SchedulerTimeType.Stride, rhs: Foundation.OperationQueue.SchedulerTimeType.Stride)
|
||||
public static func -= (lhs: inout Foundation.OperationQueue.SchedulerTimeType.Stride, rhs: Foundation.OperationQueue.SchedulerTimeType.Stride)
|
||||
public static func seconds(_ s: Swift.Int) -> Foundation.OperationQueue.SchedulerTimeType.Stride
|
||||
public static func seconds(_ s: Swift.Double) -> Foundation.OperationQueue.SchedulerTimeType.Stride
|
||||
public static func milliseconds(_ ms: Swift.Int) -> Foundation.OperationQueue.SchedulerTimeType.Stride
|
||||
public static func microseconds(_ us: Swift.Int) -> Foundation.OperationQueue.SchedulerTimeType.Stride
|
||||
public static func nanoseconds(_ ns: Swift.Int) -> Foundation.OperationQueue.SchedulerTimeType.Stride
|
||||
public init(from decoder: Swift.Decoder) throws
|
||||
public func encode(to encoder: Swift.Encoder) throws
|
||||
public static func == (a: Foundation.OperationQueue.SchedulerTimeType.Stride, b: Foundation.OperationQueue.SchedulerTimeType.Stride) -> Swift.Bool
|
||||
}
|
||||
public init(from decoder: Swift.Decoder) throws
|
||||
public func encode(to encoder: Swift.Encoder) throws
|
||||
public var hashValue: Swift.Int {
|
||||
get
|
||||
}
|
||||
public func hash(into hasher: inout Swift.Hasher)
|
||||
}
|
||||
public struct SchedulerOptions {
|
||||
}
|
||||
public func schedule(options: Foundation.OperationQueue.SchedulerOptions?, _ action: @escaping () -> Swift.Void)
|
||||
public func schedule(after date: Foundation.OperationQueue.SchedulerTimeType, tolerance: Foundation.OperationQueue.SchedulerTimeType.Stride, options: Foundation.OperationQueue.SchedulerOptions?, _ action: @escaping () -> Swift.Void)
|
||||
public func schedule(after date: Foundation.OperationQueue.SchedulerTimeType, interval: Foundation.OperationQueue.SchedulerTimeType.Stride, tolerance: Foundation.OperationQueue.SchedulerTimeType.Stride, options: Foundation.OperationQueue.SchedulerOptions?, _ action: @escaping () -> Swift.Void) -> Combine.Cancellable
|
||||
public var now: Foundation.OperationQueue.SchedulerTimeType {
|
||||
get
|
||||
}
|
||||
public var minimumTolerance: Foundation.OperationQueue.SchedulerTimeType.Stride {
|
||||
get
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <cstdlib>
|
||||
#include <system_error>
|
||||
#include <pthread.h>
|
||||
#include <signal.h>
|
||||
|
||||
#ifdef __APPLE__
|
||||
#include <os/lock.h>
|
||||
@@ -235,4 +236,8 @@ void opencombine_unfair_recursive_lock_dealloc(OpenCombineUnfairRecursiveLock lo
|
||||
return delete static_cast<PlatformIndependentMutex*>(lock.opaque);
|
||||
}
|
||||
|
||||
void opencombine_stop_in_debugger(void) {
|
||||
raise(SIGTRAP);
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
|
||||
@@ -23,50 +23,54 @@ extern "C" {
|
||||
#pragma mark - CombineIdentifier
|
||||
|
||||
uint64_t opencombine_next_combine_identifier(void)
|
||||
OPENCOMBINE_SWIFT_NAME(nextCombineIdentifier());
|
||||
OPENCOMBINE_SWIFT_NAME(__nextCombineIdentifier());
|
||||
|
||||
#pragma mark - OpenCombineUnfairLock
|
||||
|
||||
/// A wrapper around an opaque pointer for type safety in Swift.
|
||||
typedef struct OpenCombineUnfairLock {
|
||||
void* _Nonnull opaque;
|
||||
} OPENCOMBINE_SWIFT_NAME(UnfairLock) OpenCombineUnfairLock;
|
||||
} OPENCOMBINE_SWIFT_NAME(__UnfairLock) OpenCombineUnfairLock;
|
||||
|
||||
/// Allocates a lock object. The allocated object must be destroyed by calling
|
||||
/// the destroy() method.
|
||||
OpenCombineUnfairLock opencombine_unfair_lock_alloc(void)
|
||||
OPENCOMBINE_SWIFT_NAME(UnfairLock.allocate());
|
||||
OPENCOMBINE_SWIFT_NAME(__UnfairLock.allocate());
|
||||
|
||||
void opencombine_unfair_lock_lock(OpenCombineUnfairLock)
|
||||
OPENCOMBINE_SWIFT_NAME(UnfairLock.lock(self:));
|
||||
OPENCOMBINE_SWIFT_NAME(__UnfairLock.lock(self:));
|
||||
|
||||
void opencombine_unfair_lock_unlock(OpenCombineUnfairLock)
|
||||
OPENCOMBINE_SWIFT_NAME(UnfairLock.unlock(self:));
|
||||
OPENCOMBINE_SWIFT_NAME(__UnfairLock.unlock(self:));
|
||||
|
||||
void opencombine_unfair_lock_assert_owner(OpenCombineUnfairLock mutex)
|
||||
OPENCOMBINE_SWIFT_NAME(UnfairLock.assertOwner(self:));
|
||||
OPENCOMBINE_SWIFT_NAME(__UnfairLock.assertOwner(self:));
|
||||
|
||||
void opencombine_unfair_lock_dealloc(OpenCombineUnfairLock lock)
|
||||
OPENCOMBINE_SWIFT_NAME(UnfairLock.deallocate(self:));
|
||||
OPENCOMBINE_SWIFT_NAME(__UnfairLock.deallocate(self:));
|
||||
|
||||
#pragma mark - OpenCombineUnfairRecursiveLock
|
||||
|
||||
/// A wrapper around an opaque pointer for type safety in Swift.
|
||||
typedef struct OpenCombineUnfairRecursiveLock {
|
||||
void* _Nonnull opaque;
|
||||
} OPENCOMBINE_SWIFT_NAME(UnfairRecursiveLock) OpenCombineUnfairRecursiveLock;
|
||||
} OPENCOMBINE_SWIFT_NAME(__UnfairRecursiveLock) OpenCombineUnfairRecursiveLock;
|
||||
|
||||
OpenCombineUnfairRecursiveLock opencombine_unfair_recursive_lock_alloc(void)
|
||||
OPENCOMBINE_SWIFT_NAME(UnfairRecursiveLock.allocate());
|
||||
OPENCOMBINE_SWIFT_NAME(__UnfairRecursiveLock.allocate());
|
||||
|
||||
void opencombine_unfair_recursive_lock_lock(OpenCombineUnfairRecursiveLock)
|
||||
OPENCOMBINE_SWIFT_NAME(UnfairRecursiveLock.lock(self:));
|
||||
OPENCOMBINE_SWIFT_NAME(__UnfairRecursiveLock.lock(self:));
|
||||
|
||||
void opencombine_unfair_recursive_lock_unlock(OpenCombineUnfairRecursiveLock)
|
||||
OPENCOMBINE_SWIFT_NAME(UnfairRecursiveLock.unlock(self:));
|
||||
OPENCOMBINE_SWIFT_NAME(__UnfairRecursiveLock.unlock(self:));
|
||||
|
||||
void opencombine_unfair_recursive_lock_dealloc(OpenCombineUnfairRecursiveLock lock)
|
||||
OPENCOMBINE_SWIFT_NAME(UnfairRecursiveLock.deallocate(self:));
|
||||
OPENCOMBINE_SWIFT_NAME(__UnfairRecursiveLock.deallocate(self:));
|
||||
|
||||
#pragma mark - Breakpoint
|
||||
|
||||
void opencombine_stop_in_debugger(void) OPENCOMBINE_SWIFT_NAME(__stopInDebugger());
|
||||
|
||||
#ifdef __cplusplus
|
||||
} // extern "C"
|
||||
|
||||
@@ -57,7 +57,7 @@ extension AnyCancellable {
|
||||
|
||||
/// Stores this AnyCancellable in the specified set.
|
||||
/// Parameters:
|
||||
/// - collection: The set to store this AnyCancellable.
|
||||
/// - set: The set to store this AnyCancellable.
|
||||
public func store(in set: inout Set<AnyCancellable>) {
|
||||
set.insert(self)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ extension Publisher {
|
||||
///
|
||||
/// Use `eraseToAnyPublisher()` to expose an instance of `AnyPublisher` to
|
||||
/// the downstream subscriber, rather than this publisher’s actual type.
|
||||
@inlinable
|
||||
public func eraseToAnyPublisher() -> AnyPublisher<Output, Failure> {
|
||||
return .init(self)
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ extension Cancellable {
|
||||
|
||||
/// Stores this Cancellable in the specified set.
|
||||
/// Parameters:
|
||||
/// - collection: The set to store this Cancellable.
|
||||
/// - set: The set to store this Cancellable.
|
||||
public func store(in set: inout Set<AnyCancellable>) {
|
||||
AnyCancellable(self).store(in: &set)
|
||||
}
|
||||
|
||||
@@ -5,14 +5,16 @@
|
||||
// Created by Sergej Jaskiewicz on 10.06.2019.
|
||||
//
|
||||
|
||||
import func COpenCombineHelpers.nextCombineIdentifier
|
||||
#if canImport(COpenCombineHelpers)
|
||||
import COpenCombineHelpers
|
||||
#endif
|
||||
|
||||
public struct CombineIdentifier: Hashable, CustomStringConvertible {
|
||||
|
||||
private let value: UInt64
|
||||
|
||||
public init() {
|
||||
value = nextCombineIdentifier()
|
||||
value = __nextCombineIdentifier()
|
||||
}
|
||||
|
||||
public init(_ obj: AnyObject) {
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
// Created by Sergej Jaskiewicz on 11.06.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
/// A subject that wraps a single value and publishes a new element whenever the value
|
||||
/// changes.
|
||||
public final class CurrentValueSubject<Output, Failure: Error>: Subject {
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
// Created by Max Desiatov on 24/11/2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
/// A publisher that eventually produces one value and then finishes or fails.
|
||||
public final class Future<Output, Failure>: Publisher where Failure: Error {
|
||||
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
// Created by Sergej Jaskiewicz on 23.10.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
/// A helper class that acts like both subscriber and subscription.
|
||||
///
|
||||
/// Filter-like operators send an instance of their `Inner` class that is subclass
|
||||
|
||||
@@ -5,7 +5,12 @@
|
||||
// Created by Sergej Jaskiewicz on 11.06.2019.
|
||||
//
|
||||
|
||||
#if canImport(COpenCombineHelpers)
|
||||
import COpenCombineHelpers
|
||||
#endif
|
||||
|
||||
internal typealias UnfairLock = __UnfairLock
|
||||
internal typealias UnfairRecursiveLock = __UnfairRecursiveLock
|
||||
|
||||
extension UnfairRecursiveLock {
|
||||
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
// Created by Sergej Jaskiewicz on 22.09.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
/// A helper class that acts like both subscriber and subscription.
|
||||
///
|
||||
/// Reduce-like operators send an instance of their `Inner` class that is subclass
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
// Created by Sergej Jaskiewicz on 16/09/2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
// NOTE: This class has been audited for thread safety.
|
||||
internal final class SubjectSubscriber<Downstream: Subject>
|
||||
: Subscriber,
|
||||
@@ -42,23 +40,21 @@ internal final class SubjectSubscriber<Downstream: Subject>
|
||||
|
||||
internal func receive(_ input: Downstream.Output) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
guard let downstreamSubject = downstreamSubject else {
|
||||
guard let subject = downstreamSubject, upstreamSubscription != nil else {
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
guard upstreamSubscription != nil else { APIViolationValueBeforeSubscription() }
|
||||
lock.unlock()
|
||||
downstreamSubject.send(input)
|
||||
subject.send(input)
|
||||
return .none
|
||||
}
|
||||
|
||||
internal func receive(completion: Subscribers.Completion<Downstream.Failure>) {
|
||||
lock.lock()
|
||||
guard let subject = downstreamSubject else {
|
||||
guard let subject = downstreamSubject, upstreamSubscription != nil else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
guard upstreamSubscription != nil else { APIViolationUnexpectedCompletion() }
|
||||
lock.unlock()
|
||||
subject.send(completion: completion)
|
||||
downstreamSubject = nil
|
||||
@@ -89,11 +85,7 @@ internal final class SubjectSubscriber<Downstream: Subject>
|
||||
|
||||
internal func cancel() {
|
||||
lock.lock()
|
||||
if isCancelled {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
guard let subscription = upstreamSubscription else {
|
||||
guard !isCancelled, let subscription = upstreamSubscription else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -7,8 +7,9 @@
|
||||
|
||||
/// A scheduler for performing synchronous actions.
|
||||
///
|
||||
/// You can use this scheduler for immediate actions. If you attempt to schedule
|
||||
/// actions after a specific date, the scheduler produces a fatal error.
|
||||
/// You can only use this scheduler for immediate actions. If you attempt to schedule
|
||||
/// actions after a specific date, this scheduler ignores the date and performs
|
||||
/// them immediately.
|
||||
public struct ImmediateScheduler: Scheduler {
|
||||
|
||||
/// The time type used by the immediate scheduler.
|
||||
|
||||
@@ -67,19 +67,126 @@ public final class ObservableObjectPublisher: Publisher {
|
||||
|
||||
public typealias Failure = Never
|
||||
|
||||
private let subject: PassthroughSubject<Void, Never>
|
||||
private let lock = UnfairLock.allocate()
|
||||
|
||||
public init() {
|
||||
subject = .init()
|
||||
private var connections = Set<Conduit>()
|
||||
|
||||
// TODO: Combine needs this for some reason
|
||||
private var identifier: ObjectIdentifier?
|
||||
|
||||
public init() {}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Input == Void, Downstream.Failure == Never
|
||||
{
|
||||
subject.subscribe(subscriber)
|
||||
let inner = Inner(downstream: subscriber, parent: self)
|
||||
lock.lock()
|
||||
connections.insert(inner)
|
||||
lock.unlock()
|
||||
subscriber.receive(subscription: inner)
|
||||
}
|
||||
|
||||
public func send() {
|
||||
subject.send()
|
||||
lock.lock()
|
||||
let connections = self.connections
|
||||
lock.unlock()
|
||||
for connection in connections {
|
||||
connection.send()
|
||||
}
|
||||
}
|
||||
|
||||
private func remove(_ conduit: Conduit) {
|
||||
lock.lock()
|
||||
connections.remove(conduit)
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
extension ObservableObjectPublisher {
|
||||
private class Conduit: Hashable {
|
||||
|
||||
fileprivate func send() {
|
||||
abstractMethod()
|
||||
}
|
||||
|
||||
fileprivate static func == (lhs: Conduit, rhs: Conduit) -> Bool {
|
||||
return lhs === rhs
|
||||
}
|
||||
|
||||
fileprivate func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(ObjectIdentifier(self))
|
||||
}
|
||||
}
|
||||
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Conduit,
|
||||
Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Void, Downstream.Failure == Never
|
||||
{
|
||||
private enum State {
|
||||
case initialized
|
||||
case active
|
||||
case terminal
|
||||
}
|
||||
|
||||
private weak var parent: ObservableObjectPublisher?
|
||||
private let downstream: Downstream
|
||||
private let downstreamLock = UnfairRecursiveLock.allocate()
|
||||
private let lock = UnfairLock.allocate()
|
||||
private var state = State.initialized
|
||||
|
||||
init(downstream: Downstream, parent: ObservableObjectPublisher) {
|
||||
self.parent = parent
|
||||
self.downstream = downstream
|
||||
}
|
||||
|
||||
deinit {
|
||||
downstreamLock.deallocate()
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
override func send() {
|
||||
lock.lock()
|
||||
let state = self.state
|
||||
lock.unlock()
|
||||
if state == .active {
|
||||
downstreamLock.lock()
|
||||
_ = downstream.receive()
|
||||
downstreamLock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
lock.lock()
|
||||
if state == .initialized {
|
||||
state = .active
|
||||
}
|
||||
lock.unlock()
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
state = .terminal
|
||||
lock.unlock()
|
||||
parent?.remove(self)
|
||||
}
|
||||
|
||||
var description: String { return "ObservableObjectPublisher" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
let children = CollectionOfOne<Mirror.Child>(("downstream", downstream))
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
var playgroundDescription: Any {
|
||||
return description
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
// Created by Sergej Jaskiewicz on 11.06.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
/// A subject that passes along values and completion.
|
||||
///
|
||||
/// Use a `PassthroughSubject` in unit tests when you want a publisher than can publish
|
||||
|
||||
@@ -18,8 +18,7 @@
|
||||
@propertyWrapper
|
||||
public struct Published<Value> {
|
||||
|
||||
/// Initialize the storage of the `Published` property as well as the corresponding
|
||||
/// `Publisher`.
|
||||
@inlinable // trivially forwarding
|
||||
public init(initialValue: Value) {
|
||||
self.init(wrappedValue: initialValue)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,620 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ┃
|
||||
// ┃ Auto-generated from GYB template. DO NOT EDIT! ┃
|
||||
// ┃ ┃
|
||||
// ┃ ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
//
|
||||
// Publishers.Catch.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 25.12.2019.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Handles errors from an upstream publisher by replacing it with another publisher.
|
||||
///
|
||||
/// The following example replaces any error from the upstream publisher and replaces
|
||||
/// the upstream with a `Just` publisher. This continues the stream by publishing
|
||||
/// a single value and completing normally.
|
||||
/// ```
|
||||
/// enum SimpleError: Error { case error }
|
||||
/// let errorPublisher = (0..<10).publisher.tryMap { v -> Int in
|
||||
/// if v < 5 {
|
||||
/// return v
|
||||
/// } else {
|
||||
/// throw SimpleError.error
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// let noErrorPublisher = errorPublisher.catch { _ in
|
||||
/// return Just(100)
|
||||
/// }
|
||||
/// ```
|
||||
/// Backpressure note: This publisher passes through `request` and `cancel` to
|
||||
/// the upstream. After receiving an error, the publisher sends sends any unfulfilled
|
||||
/// demand to the new `Publisher`.
|
||||
///
|
||||
/// - Parameter handler: A closure that accepts the upstream failure as input and
|
||||
/// returns a publisher to replace the upstream publisher.
|
||||
/// - Returns: A publisher that handles errors from an upstream publisher by replacing
|
||||
/// the failed publisher with another publisher.
|
||||
public func `catch`<NewPublisher: Publisher>(
|
||||
_ handler: @escaping (Failure) -> NewPublisher
|
||||
) -> Publishers.Catch<Self, NewPublisher>
|
||||
where NewPublisher.Output == Output
|
||||
{
|
||||
return .init(upstream: self, handler: handler)
|
||||
}
|
||||
|
||||
/// Handles errors from an upstream publisher by either replacing it with another
|
||||
/// publisher or `throw`ing a new error.
|
||||
///
|
||||
/// - Parameter handler: A `throw`ing closure that accepts the upstream failure as
|
||||
/// input and returns a publisher to replace the upstream publisher or if an error
|
||||
/// is thrown will send the error downstream.
|
||||
/// - Returns: A publisher that handles errors from an upstream publisher by replacing
|
||||
/// the failed publisher with another publisher.
|
||||
public func tryCatch<NewPublisher: Publisher>(
|
||||
_ handler: @escaping (Failure) throws -> NewPublisher
|
||||
) -> Publishers.TryCatch<Self, NewPublisher>
|
||||
where NewPublisher.Output == Output
|
||||
{
|
||||
return .init(upstream: self, handler: handler)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that handles errors from an upstream publisher by replacing the failed
|
||||
/// publisher with another publisher.
|
||||
public struct Catch<Upstream: Publisher, NewPublisher: Publisher>: Publisher
|
||||
where Upstream.Output == NewPublisher.Output
|
||||
{
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = NewPublisher.Failure
|
||||
|
||||
/// The publisher that this publisher receives elements from.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// A closure that accepts the upstream failure as input and returns a publisher
|
||||
/// to replace the upstream publisher.
|
||||
public let handler: (Upstream.Failure) -> NewPublisher
|
||||
|
||||
/// Creates a publisher that handles errors from an upstream publisher by
|
||||
/// replacing the failed publisher with another publisher.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - upstream: The publisher that this publisher receives elements from.
|
||||
/// - handler: A closure that accepts the upstream failure as input and returns
|
||||
/// a publisher to replace the upstream publisher.
|
||||
public init(upstream: Upstream,
|
||||
handler: @escaping (Upstream.Failure) -> NewPublisher) {
|
||||
self.upstream = upstream
|
||||
self.handler = handler
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Input == Output, Downstream.Failure == Failure
|
||||
{
|
||||
let inner = Inner(downstream: subscriber, handler: handler)
|
||||
let uncaughtS = Inner.UncaughtS(inner: inner)
|
||||
upstream.subscribe(uncaughtS)
|
||||
}
|
||||
}
|
||||
|
||||
/// A publisher that handles errors from an upstream publisher by replacing the failed
|
||||
/// publisher with another publisher or optionally producing a new error.
|
||||
public struct TryCatch<Upstream: Publisher, NewPublisher: Publisher>: Publisher
|
||||
where Upstream.Output == NewPublisher.Output
|
||||
{
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Error
|
||||
|
||||
public let upstream: Upstream
|
||||
|
||||
public let handler: (Upstream.Failure) throws -> NewPublisher
|
||||
|
||||
public init(upstream: Upstream,
|
||||
handler: @escaping (Upstream.Failure) throws -> NewPublisher) {
|
||||
self.upstream = upstream
|
||||
self.handler = handler
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Input == Output, Downstream.Failure == Failure
|
||||
{
|
||||
let inner = Inner(downstream: subscriber, handler: handler)
|
||||
let uncaughtS = Inner.UncaughtS(inner: inner)
|
||||
upstream.subscribe(uncaughtS)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.Catch {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Upstream.Output,
|
||||
Downstream.Failure == NewPublisher.Failure
|
||||
{
|
||||
struct UncaughtS: Subscriber,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
let inner: Inner
|
||||
|
||||
var combineIdentifier: CombineIdentifier { return inner.combineIdentifier }
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
inner.receivePre(subscription: subscription)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
return inner.receivePre(input)
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
return inner.receivePre(completion: completion)
|
||||
}
|
||||
|
||||
var description: String { return inner.description }
|
||||
|
||||
var customMirror: Mirror { return inner.customMirror }
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
|
||||
struct CaughtS: Subscriber,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
{
|
||||
typealias Input = NewPublisher.Output
|
||||
|
||||
typealias Failure = NewPublisher.Failure
|
||||
|
||||
let inner: Inner
|
||||
|
||||
var combineIdentifier: CombineIdentifier { return inner.combineIdentifier }
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
inner.receivePost(subscription: subscription)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
return inner.receivePost(input)
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
inner.receivePost(completion: completion)
|
||||
}
|
||||
|
||||
var description: String { return inner.description }
|
||||
|
||||
var customMirror: Mirror { return inner.customMirror }
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
|
||||
private enum State {
|
||||
case pendingPre
|
||||
case pre(Subscription)
|
||||
case pendingPost
|
||||
case post(Subscription)
|
||||
case cancelled
|
||||
}
|
||||
|
||||
private let lock = UnfairLock.allocate()
|
||||
|
||||
private var demand = Subscribers.Demand.none
|
||||
|
||||
private var state = State.pendingPre
|
||||
|
||||
private let downstream: Downstream
|
||||
|
||||
private let handler: (Upstream.Failure) -> NewPublisher
|
||||
|
||||
init(downstream: Downstream,
|
||||
handler: @escaping (Upstream.Failure) -> NewPublisher) {
|
||||
self.downstream = downstream
|
||||
self.handler = handler
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
func receivePre(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard case .pendingPre = state else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
state = .pre(subscription)
|
||||
lock.unlock()
|
||||
downstream.receive(subscription: self)
|
||||
}
|
||||
|
||||
func receivePre(_ input: Upstream.Output) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
demand -= 1
|
||||
lock.unlock()
|
||||
let newDemand = downstream.receive(input)
|
||||
lock.lock()
|
||||
demand += newDemand
|
||||
lock.unlock()
|
||||
return newDemand
|
||||
}
|
||||
|
||||
func receivePre(completion: Subscribers.Completion<Upstream.Failure>) {
|
||||
switch completion {
|
||||
case .finished:
|
||||
lock.lock()
|
||||
switch state {
|
||||
case .pre:
|
||||
state = .cancelled
|
||||
lock.unlock()
|
||||
downstream.receive(completion: .finished)
|
||||
case .pendingPre, .pendingPost, .post, .cancelled:
|
||||
lock.unlock()
|
||||
}
|
||||
case .failure(let error):
|
||||
lock.lock()
|
||||
switch state {
|
||||
case .pre:
|
||||
state = .pendingPost
|
||||
lock.unlock()
|
||||
handler(error).subscribe(CaughtS(inner: self))
|
||||
case .cancelled:
|
||||
lock.unlock()
|
||||
case .pendingPre, .post, .pendingPost:
|
||||
completionBeforeSubscription()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func receivePost(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard case .pendingPost = state else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
state = .post(subscription)
|
||||
let demand = self.demand
|
||||
lock.unlock()
|
||||
if demand > 0 {
|
||||
subscription.request(demand)
|
||||
}
|
||||
}
|
||||
|
||||
func receivePost(_ input: NewPublisher.Output) -> Subscribers.Demand {
|
||||
return downstream.receive(input)
|
||||
}
|
||||
|
||||
func receivePost(completion: Subscribers.Completion<NewPublisher.Failure>) {
|
||||
lock.lock()
|
||||
guard case .post = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
state = .cancelled
|
||||
lock.unlock()
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
demand.assertNonZero()
|
||||
lock.lock()
|
||||
switch state {
|
||||
case .pendingPre:
|
||||
// The client is only able to call the `request` method after we've sent
|
||||
// `self` downstream. We only do it in the `receivePre(subscription:)`
|
||||
// method, after setting `state` to `pre`.
|
||||
// After that `state` never becomes `pendingPre`.
|
||||
requestBeforeSubscription()
|
||||
case let .pre(subscription):
|
||||
self.demand += demand
|
||||
lock.unlock()
|
||||
subscription.request(demand)
|
||||
case .pendingPost:
|
||||
self.demand += demand
|
||||
lock.unlock()
|
||||
case let .post(subscription):
|
||||
lock.unlock()
|
||||
subscription.request(demand)
|
||||
case .cancelled:
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
switch state {
|
||||
case let .pre(subscription), let .post(subscription):
|
||||
state = .cancelled
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
case .pendingPre, .pendingPost, .cancelled:
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
var description: String { return "Catch" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
let children: [Mirror.Child] = [
|
||||
("downstream", downstream),
|
||||
("demand", demand)
|
||||
]
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.TryCatch {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Upstream.Output,
|
||||
Downstream.Failure == Error
|
||||
{
|
||||
struct UncaughtS: Subscriber,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
let inner: Inner
|
||||
|
||||
var combineIdentifier: CombineIdentifier { return inner.combineIdentifier }
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
inner.receivePre(subscription: subscription)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
return inner.receivePre(input)
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
return inner.receivePre(completion: completion)
|
||||
}
|
||||
|
||||
var description: String { return inner.description }
|
||||
|
||||
var customMirror: Mirror { return inner.customMirror }
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
|
||||
struct CaughtS: Subscriber,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
{
|
||||
typealias Input = NewPublisher.Output
|
||||
|
||||
typealias Failure = NewPublisher.Failure
|
||||
|
||||
let inner: Inner
|
||||
|
||||
var combineIdentifier: CombineIdentifier { return inner.combineIdentifier }
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
inner.receivePost(subscription: subscription)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
return inner.receivePost(input)
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
inner.receivePost(completion: completion)
|
||||
}
|
||||
|
||||
var description: String { return inner.description }
|
||||
|
||||
var customMirror: Mirror { return inner.customMirror }
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
|
||||
private enum State {
|
||||
case pendingPre
|
||||
case pre(Subscription)
|
||||
case pendingPost
|
||||
case post(Subscription)
|
||||
case cancelled
|
||||
}
|
||||
|
||||
private let lock = UnfairLock.allocate()
|
||||
|
||||
private var demand = Subscribers.Demand.none
|
||||
|
||||
private var state = State.pendingPre
|
||||
|
||||
private let downstream: Downstream
|
||||
|
||||
private let handler: (Upstream.Failure) throws -> NewPublisher
|
||||
|
||||
init(downstream: Downstream,
|
||||
handler: @escaping (Upstream.Failure) throws -> NewPublisher) {
|
||||
self.downstream = downstream
|
||||
self.handler = handler
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
func receivePre(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard case .pendingPre = state else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
state = .pre(subscription)
|
||||
lock.unlock()
|
||||
downstream.receive(subscription: self)
|
||||
}
|
||||
|
||||
func receivePre(_ input: Upstream.Output) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
demand -= 1
|
||||
lock.unlock()
|
||||
let newDemand = downstream.receive(input)
|
||||
lock.lock()
|
||||
demand += newDemand
|
||||
lock.unlock()
|
||||
return newDemand
|
||||
}
|
||||
|
||||
func receivePre(completion: Subscribers.Completion<Upstream.Failure>) {
|
||||
switch completion {
|
||||
case .finished:
|
||||
lock.lock()
|
||||
switch state {
|
||||
case .pre:
|
||||
state = .cancelled
|
||||
lock.unlock()
|
||||
downstream.receive(completion: .finished)
|
||||
case .pendingPre, .pendingPost, .post, .cancelled:
|
||||
lock.unlock()
|
||||
}
|
||||
case .failure(let error):
|
||||
lock.lock()
|
||||
switch state {
|
||||
case .pre:
|
||||
state = .pendingPost
|
||||
lock.unlock()
|
||||
do {
|
||||
try handler(error).subscribe(CaughtS(inner: self))
|
||||
} catch let anotherError {
|
||||
lock.lock()
|
||||
state = .cancelled
|
||||
lock.unlock()
|
||||
downstream.receive(completion: .failure(anotherError))
|
||||
}
|
||||
case .cancelled:
|
||||
lock.unlock()
|
||||
case .pendingPre, .post, .pendingPost:
|
||||
completionBeforeSubscription()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func receivePost(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard case .pendingPost = state else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
state = .post(subscription)
|
||||
let demand = self.demand
|
||||
lock.unlock()
|
||||
if demand > 0 {
|
||||
subscription.request(demand)
|
||||
}
|
||||
}
|
||||
|
||||
func receivePost(_ input: NewPublisher.Output) -> Subscribers.Demand {
|
||||
return downstream.receive(input)
|
||||
}
|
||||
|
||||
func receivePost(completion: Subscribers.Completion<NewPublisher.Failure>) {
|
||||
lock.lock()
|
||||
guard case .post = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
state = .cancelled
|
||||
lock.unlock()
|
||||
downstream.receive(completion: completion.eraseError())
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
demand.assertNonZero()
|
||||
lock.lock()
|
||||
switch state {
|
||||
case .pendingPre:
|
||||
// The client is only able to call the `request` method after we've sent
|
||||
// `self` downstream. We only do it in the `receivePre(subscription:)`
|
||||
// method, after setting `state` to `pre`.
|
||||
// After that `state` never becomes `pendingPre`.
|
||||
requestBeforeSubscription()
|
||||
case let .pre(subscription):
|
||||
self.demand += demand
|
||||
lock.unlock()
|
||||
subscription.request(demand)
|
||||
case .pendingPost:
|
||||
self.demand += demand
|
||||
lock.unlock()
|
||||
case let .post(subscription):
|
||||
lock.unlock()
|
||||
subscription.request(demand)
|
||||
case .cancelled:
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
switch state {
|
||||
case let .pre(subscription), let .post(subscription):
|
||||
state = .cancelled
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
case .pendingPre, .pendingPost, .cancelled:
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
var description: String { return "TryCatch" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
let children: [Mirror.Child] = [
|
||||
("downstream", downstream),
|
||||
("demand", demand)
|
||||
]
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
|
||||
private func completionBeforeSubscription(file: StaticString = #file,
|
||||
line: UInt = #line) -> Never {
|
||||
fatalError("Unexpected state: received completion but do not have subscription",
|
||||
file: file,
|
||||
line: line)
|
||||
}
|
||||
|
||||
private func requestBeforeSubscription(file: StaticString = #file,
|
||||
line: UInt = #line) -> Never {
|
||||
fatalError("Unexpected state: request before subscription sent",
|
||||
file: file,
|
||||
line: line)
|
||||
}
|
||||
@@ -249,6 +249,26 @@ extension Just {
|
||||
) -> Result<ElementOfResult, Error>.OCombine.Publisher {
|
||||
return .init(Result { try nextPartialResult(initialResult, output) })
|
||||
}
|
||||
|
||||
public func prepend(_ elements: Output...) -> Publishers.Sequence<[Output], Never> {
|
||||
return prepend(elements)
|
||||
}
|
||||
|
||||
public func prepend<Elements: Sequence>(
|
||||
_ elements: Elements
|
||||
) -> Publishers.Sequence<[Output], Never> where Output == Elements.Element {
|
||||
return .init(sequence: elements + [output])
|
||||
}
|
||||
|
||||
public func append(_ elements: Output...) -> Publishers.Sequence<[Output], Never> {
|
||||
return append(elements)
|
||||
}
|
||||
|
||||
public func append<Elements: Sequence>(
|
||||
_ elements: Elements
|
||||
) -> Publishers.Sequence<[Output], Never> where Output == Elements.Element {
|
||||
return .init(sequence: [output] + elements)
|
||||
}
|
||||
}
|
||||
|
||||
extension Just {
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
//
|
||||
// Publishers.AssertNoFailure.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 25.12.2019.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Raises a fatal error when its upstream publisher fails, and otherwise republishes
|
||||
/// all received input.
|
||||
///
|
||||
/// Use this function for internal sanity checks that are active during testing but
|
||||
/// do not impact performance of shipping code.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - prefix: A string used at the beginning of the fatal error message.
|
||||
/// - file: A filename used in the error message. This defaults to `#file`.
|
||||
/// - line: A line number used in the error message. This defaults to `#line`.
|
||||
/// - Returns: A publisher that raises a fatal error when its upstream publisher
|
||||
/// fails.
|
||||
public func assertNoFailure(_ prefix: String = "",
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line) -> Publishers.AssertNoFailure<Self> {
|
||||
return .init(upstream: self, prefix: prefix, file: file, line: line)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that raises a fatal error upon receiving any failure, and otherwise
|
||||
/// republishes all received input.
|
||||
///
|
||||
/// Use this function for internal sanity checks that are active during testing but
|
||||
/// do not impact performance of shipping code.
|
||||
public struct AssertNoFailure<Upstream: Publisher>: Publisher {
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Never
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The string used at the beginning of the fatal error message.
|
||||
public let prefix: String
|
||||
|
||||
/// The filename used in the error message.
|
||||
public let file: StaticString
|
||||
|
||||
/// The line number used in the error message.
|
||||
public let line: UInt
|
||||
|
||||
public init(upstream: Upstream, prefix: String, file: StaticString, line: UInt) {
|
||||
self.upstream = upstream
|
||||
self.prefix = prefix
|
||||
self.file = file
|
||||
self.line = line
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Input == Output, Downstream.Failure == Never
|
||||
{
|
||||
upstream.subscribe(Inner(downstream: subscriber,
|
||||
prefix: prefix,
|
||||
file: file,
|
||||
line: line))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.AssertNoFailure {
|
||||
private struct Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Upstream.Output, Downstream.Failure == Never
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
private let downstream: Downstream
|
||||
|
||||
private let prefix: String
|
||||
|
||||
private let file: StaticString
|
||||
|
||||
private let line: UInt
|
||||
|
||||
let combineIdentifier = CombineIdentifier()
|
||||
|
||||
init(downstream: Downstream, prefix: String, file: StaticString, line: UInt) {
|
||||
self.downstream = downstream
|
||||
self.prefix = prefix
|
||||
self.file = file
|
||||
self.line = line
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
downstream.receive(subscription: subscription)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
return downstream.receive(input)
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
switch completion {
|
||||
case .finished:
|
||||
downstream.receive(completion: .finished)
|
||||
case .failure(let error):
|
||||
let prefix = self.prefix.isEmpty ? "" : self.prefix + ": "
|
||||
fatalError("\(prefix)\(error)", file: file, line: line)
|
||||
}
|
||||
}
|
||||
|
||||
var description: String { return "AssertNoFailure" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
let children: [Mirror.Child] = [
|
||||
("file", file),
|
||||
("line", line),
|
||||
("prefix", prefix)
|
||||
]
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,6 @@
|
||||
// Created by Sergej Jaskiewicz on 18/09/2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
extension ConnectablePublisher {
|
||||
|
||||
/// Automates the process of connecting or disconnecting from this connectable
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
//
|
||||
// Publishers.Breakpoint.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 03.12.2019.
|
||||
//
|
||||
|
||||
#if canImport(COpenCombineHelpers)
|
||||
import COpenCombineHelpers
|
||||
#endif
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Raises a debugger signal when a provided closure needs to stop the process in
|
||||
/// the debugger.
|
||||
///
|
||||
/// When any of the provided closures returns `true`, this publisher raises
|
||||
/// the `SIGTRAP` signal to stop the process in the debugger.
|
||||
/// Otherwise, this publisher passes through values and completions as-is.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - receiveSubscription: A closure that executes when when the publisher receives
|
||||
/// a subscription. Return `true` from this closure to raise `SIGTRAP`, or `false`
|
||||
/// to continue.
|
||||
/// - receiveOutput: A closure that executes when when the publisher receives
|
||||
/// a value. Return `true` from this closure to raise `SIGTRAP`, or `false`
|
||||
/// to continue.
|
||||
/// - receiveCompletion: A closure that executes when when the publisher receives
|
||||
/// a completion. Return `true` from this closure to raise `SIGTRAP`, or `false`
|
||||
/// to continue.
|
||||
/// - Returns: A publisher that raises a debugger signal when one of the provided
|
||||
/// closures returns `true`.
|
||||
public func breakpoint(
|
||||
receiveSubscription: ((Subscription) -> Bool)? = nil,
|
||||
receiveOutput: ((Output) -> Bool)? = nil,
|
||||
receiveCompletion: ((Subscribers.Completion<Failure>) -> Bool)? = nil
|
||||
) -> Publishers.Breakpoint<Self> {
|
||||
return .init(upstream: self,
|
||||
receiveSubscription: receiveSubscription,
|
||||
receiveOutput: receiveOutput,
|
||||
receiveCompletion: receiveCompletion)
|
||||
}
|
||||
|
||||
/// Raises a debugger signal upon receiving a failure.
|
||||
///
|
||||
/// When the upstream publisher fails with an error, this publisher raises
|
||||
/// the `SIGTRAP` signal, which stops the process in the debugger.
|
||||
/// Otherwise, this publisher passes through values and completions as-is.
|
||||
///
|
||||
/// - Returns: A publisher that raises a debugger signal upon receiving a failure.
|
||||
public func breakpointOnError() -> Publishers.Breakpoint<Self> {
|
||||
return breakpoint { completion in
|
||||
switch completion {
|
||||
case .finished:
|
||||
return false
|
||||
case .failure:
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that raises a debugger signal when a provided closure needs to stop
|
||||
/// the process in the debugger.
|
||||
///
|
||||
/// When any of the provided closures returns `true`, this publisher raises
|
||||
/// the `SIGTRAP` signal to stop the process in the debugger.
|
||||
/// Otherwise, this publisher passes through values and completions as-is.
|
||||
public struct Breakpoint<Upstream: Publisher>: Publisher {
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// A closure that executes when the publisher receives a subscription, and can
|
||||
/// raise a debugger signal by returning a `true` Boolean value.
|
||||
public let receiveSubscription: ((Subscription) -> Bool)?
|
||||
|
||||
/// A closure that executes when the publisher receives output from the upstream
|
||||
/// publisher, and can raise a debugger signal by returning a `true` Boolean
|
||||
/// value.
|
||||
public let receiveOutput: ((Upstream.Output) -> Bool)?
|
||||
|
||||
/// A closure that executes when the publisher receives completion, and can raise
|
||||
/// a debugger signal by returning a `true` Boolean value.
|
||||
public let receiveCompletion:
|
||||
((Subscribers.Completion<Upstream.Failure>) -> Bool)?
|
||||
|
||||
/// Creates a breakpoint publisher with the provided upstream publisher and
|
||||
/// breakpoint-raising closures.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - upstream: The publisher from which this publisher receives elements.
|
||||
/// - receiveSubscription: A closure that executes when the publisher receives
|
||||
/// a subscription, and can raise a debugger signal by returning a `true`
|
||||
/// Boolean value.
|
||||
/// - receiveOutput: A closure that executes when the publisher receives output
|
||||
/// from the upstream publisher, and can raise a debugger signal by returning
|
||||
/// a `true` Boolean value.
|
||||
/// - receiveCompletion: A closure that executes when the publisher receives
|
||||
/// completion, and can raise a debugger signal by returning a `true` Boolean
|
||||
/// value.
|
||||
public init(
|
||||
upstream: Upstream,
|
||||
receiveSubscription: ((Subscription) -> Bool)? = nil,
|
||||
receiveOutput: ((Upstream.Output) -> Bool)? = nil,
|
||||
receiveCompletion: ((Subscribers.Completion<Failure>) -> Bool)? = nil
|
||||
) {
|
||||
self.upstream = upstream
|
||||
self.receiveSubscription = receiveSubscription
|
||||
self.receiveOutput = receiveOutput
|
||||
self.receiveCompletion = receiveCompletion
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Failure == Downstream.Failure,
|
||||
Upstream.Output == Downstream.Input
|
||||
{
|
||||
upstream.subscribe(Inner(self, downstream: subscriber))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.Breakpoint {
|
||||
private struct Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Upstream.Output, Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
private let downstream: Downstream
|
||||
private let breakpoint: Publishers.Breakpoint<Upstream>
|
||||
|
||||
let combineIdentifier = CombineIdentifier()
|
||||
|
||||
init(_ breakpoint: Publishers.Breakpoint<Upstream>,
|
||||
downstream: Downstream) {
|
||||
self.downstream = downstream
|
||||
self.breakpoint = breakpoint
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
if breakpoint.receiveSubscription?(subscription) == true {
|
||||
__stopInDebugger()
|
||||
}
|
||||
downstream.receive(subscription: subscription)
|
||||
}
|
||||
|
||||
func receive(_ input: Upstream.Output) -> Subscribers.Demand {
|
||||
if breakpoint.receiveOutput?(input) == true {
|
||||
__stopInDebugger()
|
||||
}
|
||||
return downstream.receive(input)
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Upstream.Failure>) {
|
||||
if breakpoint.receiveCompletion?(completion) == true {
|
||||
__stopInDebugger()
|
||||
}
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
|
||||
var description: String { return "Breakpoint" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
let children = CollectionOfOne<Mirror.Child>(
|
||||
("upstream", breakpoint.upstream)
|
||||
)
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
//
|
||||
// Publishers.Buffer.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 08.01.2020.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Buffers elements received from an upstream publisher.
|
||||
/// - Parameter size: The maximum number of elements to store.
|
||||
/// - Parameter prefetch: The strategy for initially populating the buffer.
|
||||
/// - Parameter whenFull: The action to take when the buffer becomes full.
|
||||
public func buffer(
|
||||
size: Int,
|
||||
prefetch: Publishers.PrefetchStrategy,
|
||||
whenFull: Publishers.BufferingStrategy<Failure>
|
||||
) -> Publishers.Buffer<Self> {
|
||||
return .init(upstream: self,
|
||||
size: size,
|
||||
prefetch: prefetch,
|
||||
whenFull: whenFull)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A strategy for filling a buffer.
|
||||
///
|
||||
/// * keepFull: A strategy to fill the buffer at subscription time, and keep it full
|
||||
/// thereafter.
|
||||
/// * byRequest: A strategy that avoids prefetching and instead performs requests
|
||||
/// on demand.
|
||||
public enum PrefetchStrategy {
|
||||
|
||||
/// A strategy to fill the buffer at subscription time, and keep it full
|
||||
/// thereafter.
|
||||
case keepFull
|
||||
|
||||
/// A strategy that avoids prefetching and instead performs requests
|
||||
/// on demand.
|
||||
case byRequest
|
||||
}
|
||||
|
||||
/// A strategy for handling exhaustion of a buffer’s capacity.
|
||||
///
|
||||
/// * dropNewest: When full, discard the newly-received element without buffering it.
|
||||
/// * dropOldest: When full, remove the least recently-received element from the
|
||||
/// buffer.
|
||||
/// * customError: When full, execute the closure to provide a custom error.
|
||||
public enum BufferingStrategy<Failure: Error> {
|
||||
|
||||
/// When full, discard the newly-received element without buffering it.
|
||||
case dropNewest
|
||||
|
||||
/// When full, remove the least recently-received element from the buffer.
|
||||
case dropOldest
|
||||
|
||||
/// When full, execute the closure to provide a custom error.
|
||||
case customError(() -> Failure)
|
||||
}
|
||||
|
||||
/// A publisher that buffers elements received from an upstream publisher.
|
||||
public struct Buffer<Upstream: Publisher>: Publisher {
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The maximum number of elements to store.
|
||||
public let size: Int
|
||||
|
||||
/// The strategy for initially populating the buffer.
|
||||
public let prefetch: Publishers.PrefetchStrategy
|
||||
|
||||
/// The action to take when the buffer becomes full.
|
||||
public let whenFull: Publishers.BufferingStrategy<Failure>
|
||||
|
||||
/// Creates a publisher that buffers elements received from an upstream publisher.
|
||||
/// - Parameter upstream: The publisher from which this publisher receives
|
||||
/// elements.
|
||||
/// - Parameter size: The maximum number of elements to store.
|
||||
/// - Parameter prefetch: The strategy for initially populating the buffer.
|
||||
/// - Parameter whenFull: The action to take when the buffer becomes full.
|
||||
public init(upstream: Upstream,
|
||||
size: Int,
|
||||
prefetch: Publishers.PrefetchStrategy,
|
||||
whenFull: Publishers.BufferingStrategy<Failure>) {
|
||||
self.upstream = upstream
|
||||
self.size = size
|
||||
self.prefetch = prefetch
|
||||
self.whenFull = whenFull
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Input == Output, Downstream.Failure == Failure
|
||||
{
|
||||
upstream.subscribe(Inner(downstream: subscriber, buffer: self))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.PrefetchStrategy: Equatable {}
|
||||
|
||||
extension Publishers.PrefetchStrategy: Hashable {}
|
||||
|
||||
extension Publishers.Buffer {
|
||||
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 enum State {
|
||||
case ready(Publishers.Buffer<Upstream>, Downstream)
|
||||
case subscribed(Publishers.Buffer<Upstream>, Downstream, Subscription)
|
||||
case terminal
|
||||
}
|
||||
|
||||
private let lock = UnfairLock.allocate()
|
||||
|
||||
private var recursion = false
|
||||
|
||||
private var state: State
|
||||
|
||||
private var downstreamDemand = Subscribers.Demand.none
|
||||
|
||||
// TODO: Use a deque here?
|
||||
// Need to measure performance with large buffers and `dropOldest` strategy.
|
||||
private var values = [Input]()
|
||||
|
||||
private var upstreamFailed = false
|
||||
|
||||
private var terminal: Subscribers.Completion<Failure>?
|
||||
|
||||
init(downstream: Downstream, buffer: Publishers.Buffer<Upstream>) {
|
||||
state = .ready(buffer, downstream)
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard case let .ready(buffer, downstream) = state else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
state = .subscribed(buffer, downstream, subscription)
|
||||
lock.unlock()
|
||||
|
||||
let upstreamDemand: Subscribers.Demand
|
||||
switch buffer.prefetch {
|
||||
case .keepFull:
|
||||
upstreamDemand = .max(buffer.size)
|
||||
case .byRequest:
|
||||
upstreamDemand = .unlimited
|
||||
}
|
||||
subscription.request(upstreamDemand)
|
||||
downstream.receive(subscription: self)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
guard case let .subscribed(buffer, _, subscription) = state else {
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
switch terminal {
|
||||
case nil, .finished?:
|
||||
if values.count >= buffer.size {
|
||||
switch buffer.whenFull {
|
||||
case .dropNewest:
|
||||
lock.unlock()
|
||||
return drain()
|
||||
case .dropOldest:
|
||||
values.removeFirst()
|
||||
case let .customError(makeError):
|
||||
terminal = .failure(makeError())
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
||||
values.append(input)
|
||||
lock.unlock()
|
||||
return drain()
|
||||
case .failure?:
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Upstream.Failure>) {
|
||||
lock.lock()
|
||||
guard case .subscribed = state, terminal == nil else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
terminal = completion
|
||||
lock.unlock()
|
||||
_ = drain()
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
lock.lock()
|
||||
guard case let .subscribed(_, _, subscription) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
downstreamDemand += demand
|
||||
let recursion = self.recursion
|
||||
lock.unlock()
|
||||
if recursion {
|
||||
return
|
||||
}
|
||||
|
||||
// Request the number of items just enough to fill the buffer.
|
||||
subscription.request(drain() + demand)
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
guard case let .subscribed(_, _, subscription) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
state = .terminal
|
||||
values = []
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
}
|
||||
|
||||
private func drain() -> Subscribers.Demand {
|
||||
var upstreamDemand = Subscribers.Demand.none
|
||||
lock.lock()
|
||||
while true {
|
||||
guard case let .subscribed(buffer, downstream, _) = state else {
|
||||
lock.unlock()
|
||||
return upstreamDemand
|
||||
}
|
||||
|
||||
if downstreamDemand > 0 {
|
||||
if values.isEmpty {
|
||||
if let completion = terminal {
|
||||
state = .terminal
|
||||
lock.unlock()
|
||||
downstream.receive(completion: completion)
|
||||
} else {
|
||||
lock.unlock()
|
||||
}
|
||||
return upstreamDemand
|
||||
}
|
||||
} else {
|
||||
if let completion = terminal, case .failure = completion {
|
||||
state = .terminal
|
||||
lock.unlock()
|
||||
downstream.receive(completion: completion)
|
||||
} else {
|
||||
lock.unlock()
|
||||
}
|
||||
return upstreamDemand
|
||||
}
|
||||
|
||||
let poppedValues = lockedPop(downstreamDemand)
|
||||
assert(poppedValues.count > 0,
|
||||
"""
|
||||
We check that the buffer is not empty and downstreamDemand is \
|
||||
nonzero, how can this be triggered?
|
||||
""")
|
||||
|
||||
// This should not crash because `lockedPop(_:)` returns at most
|
||||
// `downstreamDemand` items.
|
||||
downstreamDemand -= poppedValues.count
|
||||
|
||||
recursion = true
|
||||
lock.unlock()
|
||||
|
||||
var newDownstreamDemand = Subscribers.Demand.none
|
||||
var additionalUpstreamDemand = 0
|
||||
|
||||
for value in poppedValues {
|
||||
newDownstreamDemand += downstream.receive(value)
|
||||
additionalUpstreamDemand += 1
|
||||
}
|
||||
|
||||
if buffer.prefetch == .keepFull {
|
||||
upstreamDemand += additionalUpstreamDemand
|
||||
}
|
||||
|
||||
lock.lock()
|
||||
recursion = false
|
||||
downstreamDemand += newDownstreamDemand
|
||||
}
|
||||
}
|
||||
|
||||
private func lockedPop(_ demand: Subscribers.Demand) -> [Input] {
|
||||
assert(demand > 0)
|
||||
guard let max = demand.max else {
|
||||
let poppedValues = self.values
|
||||
self.values = []
|
||||
return poppedValues
|
||||
}
|
||||
|
||||
let poppedValues = Array(values.prefix(max))
|
||||
values.removeFirst(poppedValues.count)
|
||||
return poppedValues
|
||||
}
|
||||
|
||||
var description: String { return "Buffer" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
let children: [Mirror.Child] = [
|
||||
("values", values),
|
||||
("state", state),
|
||||
("downstreamDemand", downstreamDemand),
|
||||
("terminal", terminal as Any)
|
||||
]
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,401 @@
|
||||
${template_header}
|
||||
//
|
||||
// Publishers.Catch.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 25.12.2019.
|
||||
//
|
||||
|
||||
%{
|
||||
instantiations = ['Catch', 'TryCatch']
|
||||
}%
|
||||
extension Publisher {
|
||||
|
||||
/// Handles errors from an upstream publisher by replacing it with another publisher.
|
||||
///
|
||||
/// The following example replaces any error from the upstream publisher and replaces
|
||||
/// the upstream with a `Just` publisher. This continues the stream by publishing
|
||||
/// a single value and completing normally.
|
||||
/// ```
|
||||
/// enum SimpleError: Error { case error }
|
||||
/// let errorPublisher = (0..<10).publisher.tryMap { v -> Int in
|
||||
/// if v < 5 {
|
||||
/// return v
|
||||
/// } else {
|
||||
/// throw SimpleError.error
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// let noErrorPublisher = errorPublisher.catch { _ in
|
||||
/// return Just(100)
|
||||
/// }
|
||||
/// ```
|
||||
/// Backpressure note: This publisher passes through `request` and `cancel` to
|
||||
/// the upstream. After receiving an error, the publisher sends sends any unfulfilled
|
||||
/// demand to the new `Publisher`.
|
||||
///
|
||||
/// - Parameter handler: A closure that accepts the upstream failure as input and
|
||||
/// returns a publisher to replace the upstream publisher.
|
||||
/// - Returns: A publisher that handles errors from an upstream publisher by replacing
|
||||
/// the failed publisher with another publisher.
|
||||
public func `catch`<NewPublisher: Publisher>(
|
||||
_ handler: @escaping (Failure) -> NewPublisher
|
||||
) -> Publishers.Catch<Self, NewPublisher>
|
||||
where NewPublisher.Output == Output
|
||||
{
|
||||
return .init(upstream: self, handler: handler)
|
||||
}
|
||||
|
||||
/// Handles errors from an upstream publisher by either replacing it with another
|
||||
/// publisher or `throw`ing a new error.
|
||||
///
|
||||
/// - Parameter handler: A `throw`ing closure that accepts the upstream failure as
|
||||
/// input and returns a publisher to replace the upstream publisher or if an error
|
||||
/// is thrown will send the error downstream.
|
||||
/// - Returns: A publisher that handles errors from an upstream publisher by replacing
|
||||
/// the failed publisher with another publisher.
|
||||
public func tryCatch<NewPublisher: Publisher>(
|
||||
_ handler: @escaping (Failure) throws -> NewPublisher
|
||||
) -> Publishers.TryCatch<Self, NewPublisher>
|
||||
where NewPublisher.Output == Output
|
||||
{
|
||||
return .init(upstream: self, handler: handler)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that handles errors from an upstream publisher by replacing the failed
|
||||
/// publisher with another publisher.
|
||||
public struct Catch<Upstream: Publisher, NewPublisher: Publisher>: Publisher
|
||||
where Upstream.Output == NewPublisher.Output
|
||||
{
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = NewPublisher.Failure
|
||||
|
||||
/// The publisher that this publisher receives elements from.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// A closure that accepts the upstream failure as input and returns a publisher
|
||||
/// to replace the upstream publisher.
|
||||
public let handler: (Upstream.Failure) -> NewPublisher
|
||||
|
||||
/// Creates a publisher that handles errors from an upstream publisher by
|
||||
/// replacing the failed publisher with another publisher.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - upstream: The publisher that this publisher receives elements from.
|
||||
/// - handler: A closure that accepts the upstream failure as input and returns
|
||||
/// a publisher to replace the upstream publisher.
|
||||
public init(upstream: Upstream,
|
||||
handler: @escaping (Upstream.Failure) -> NewPublisher) {
|
||||
self.upstream = upstream
|
||||
self.handler = handler
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Input == Output, Downstream.Failure == Failure
|
||||
{
|
||||
let inner = Inner(downstream: subscriber, handler: handler)
|
||||
let uncaughtS = Inner.UncaughtS(inner: inner)
|
||||
upstream.subscribe(uncaughtS)
|
||||
}
|
||||
}
|
||||
|
||||
/// A publisher that handles errors from an upstream publisher by replacing the failed
|
||||
/// publisher with another publisher or optionally producing a new error.
|
||||
public struct TryCatch<Upstream: Publisher, NewPublisher: Publisher>: Publisher
|
||||
where Upstream.Output == NewPublisher.Output
|
||||
{
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Error
|
||||
|
||||
public let upstream: Upstream
|
||||
|
||||
public let handler: (Upstream.Failure) throws -> NewPublisher
|
||||
|
||||
public init(upstream: Upstream,
|
||||
handler: @escaping (Upstream.Failure) throws -> NewPublisher) {
|
||||
self.upstream = upstream
|
||||
self.handler = handler
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Input == Output, Downstream.Failure == Failure
|
||||
{
|
||||
let inner = Inner(downstream: subscriber, handler: handler)
|
||||
let uncaughtS = Inner.UncaughtS(inner: inner)
|
||||
upstream.subscribe(uncaughtS)
|
||||
}
|
||||
}
|
||||
}
|
||||
% for instantiation in instantiations:
|
||||
% throws_modifier = ' throws' if instantiation == 'TryCatch' else ''
|
||||
|
||||
extension Publishers.${instantiation} {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Upstream.Output,
|
||||
% if instantiation == 'Catch':
|
||||
Downstream.Failure == NewPublisher.Failure
|
||||
% else:
|
||||
Downstream.Failure == Error
|
||||
% end
|
||||
{
|
||||
struct UncaughtS: Subscriber,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
let inner: Inner
|
||||
|
||||
var combineIdentifier: CombineIdentifier { return inner.combineIdentifier }
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
inner.receivePre(subscription: subscription)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
return inner.receivePre(input)
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
return inner.receivePre(completion: completion)
|
||||
}
|
||||
|
||||
var description: String { return inner.description }
|
||||
|
||||
var customMirror: Mirror { return inner.customMirror }
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
|
||||
struct CaughtS: Subscriber,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
{
|
||||
typealias Input = NewPublisher.Output
|
||||
|
||||
typealias Failure = NewPublisher.Failure
|
||||
|
||||
let inner: Inner
|
||||
|
||||
var combineIdentifier: CombineIdentifier { return inner.combineIdentifier }
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
inner.receivePost(subscription: subscription)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
return inner.receivePost(input)
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
inner.receivePost(completion: completion)
|
||||
}
|
||||
|
||||
var description: String { return inner.description }
|
||||
|
||||
var customMirror: Mirror { return inner.customMirror }
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
|
||||
private enum State {
|
||||
case pendingPre
|
||||
case pre(Subscription)
|
||||
case pendingPost
|
||||
case post(Subscription)
|
||||
case cancelled
|
||||
}
|
||||
|
||||
private let lock = UnfairLock.allocate()
|
||||
|
||||
private var demand = Subscribers.Demand.none
|
||||
|
||||
private var state = State.pendingPre
|
||||
|
||||
private let downstream: Downstream
|
||||
|
||||
private let handler: (Upstream.Failure)${throws_modifier} -> NewPublisher
|
||||
|
||||
init(downstream: Downstream,
|
||||
handler: @escaping (Upstream.Failure)${throws_modifier} -> NewPublisher) {
|
||||
self.downstream = downstream
|
||||
self.handler = handler
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
func receivePre(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard case .pendingPre = state else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
state = .pre(subscription)
|
||||
lock.unlock()
|
||||
downstream.receive(subscription: self)
|
||||
}
|
||||
|
||||
func receivePre(_ input: Upstream.Output) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
demand -= 1
|
||||
lock.unlock()
|
||||
let newDemand = downstream.receive(input)
|
||||
lock.lock()
|
||||
demand += newDemand
|
||||
lock.unlock()
|
||||
return newDemand
|
||||
}
|
||||
|
||||
func receivePre(completion: Subscribers.Completion<Upstream.Failure>) {
|
||||
switch completion {
|
||||
case .finished:
|
||||
lock.lock()
|
||||
switch state {
|
||||
case .pre:
|
||||
state = .cancelled
|
||||
lock.unlock()
|
||||
downstream.receive(completion: .finished)
|
||||
case .pendingPre, .pendingPost, .post, .cancelled:
|
||||
lock.unlock()
|
||||
}
|
||||
case .failure(let error):
|
||||
lock.lock()
|
||||
switch state {
|
||||
case .pre:
|
||||
state = .pendingPost
|
||||
lock.unlock()
|
||||
% if instantiation == 'Catch':
|
||||
handler(error).subscribe(CaughtS(inner: self))
|
||||
% else:
|
||||
do {
|
||||
try handler(error).subscribe(CaughtS(inner: self))
|
||||
} catch let anotherError {
|
||||
lock.lock()
|
||||
state = .cancelled
|
||||
lock.unlock()
|
||||
downstream.receive(completion: .failure(anotherError))
|
||||
}
|
||||
% end
|
||||
case .cancelled:
|
||||
lock.unlock()
|
||||
case .pendingPre, .post, .pendingPost:
|
||||
completionBeforeSubscription()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func receivePost(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard case .pendingPost = state else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
state = .post(subscription)
|
||||
let demand = self.demand
|
||||
lock.unlock()
|
||||
if demand > 0 {
|
||||
subscription.request(demand)
|
||||
}
|
||||
}
|
||||
|
||||
func receivePost(_ input: NewPublisher.Output) -> Subscribers.Demand {
|
||||
return downstream.receive(input)
|
||||
}
|
||||
|
||||
func receivePost(completion: Subscribers.Completion<NewPublisher.Failure>) {
|
||||
lock.lock()
|
||||
guard case .post = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
state = .cancelled
|
||||
lock.unlock()
|
||||
% if instantiation == 'Catch':
|
||||
downstream.receive(completion: completion)
|
||||
% else:
|
||||
downstream.receive(completion: completion.eraseError())
|
||||
% end
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
demand.assertNonZero()
|
||||
lock.lock()
|
||||
switch state {
|
||||
case .pendingPre:
|
||||
// The client is only able to call the `request` method after we've sent
|
||||
// `self` downstream. We only do it in the `receivePre(subscription:)`
|
||||
// method, after setting `state` to `pre`.
|
||||
// After that `state` never becomes `pendingPre`.
|
||||
requestBeforeSubscription()
|
||||
case let .pre(subscription):
|
||||
self.demand += demand
|
||||
lock.unlock()
|
||||
subscription.request(demand)
|
||||
case .pendingPost:
|
||||
self.demand += demand
|
||||
lock.unlock()
|
||||
case let .post(subscription):
|
||||
lock.unlock()
|
||||
subscription.request(demand)
|
||||
case .cancelled:
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
switch state {
|
||||
case let .pre(subscription), let .post(subscription):
|
||||
state = .cancelled
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
case .pendingPre, .pendingPost, .cancelled:
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
var description: String { return "${instantiation}" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
let children: [Mirror.Child] = [
|
||||
("downstream", downstream),
|
||||
("demand", demand)
|
||||
]
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
% end
|
||||
|
||||
private func completionBeforeSubscription(file: StaticString = #file,
|
||||
line: UInt = #line) -> Never {
|
||||
fatalError("Unexpected state: received completion but do not have subscription",
|
||||
file: file,
|
||||
line: line)
|
||||
}
|
||||
|
||||
private func requestBeforeSubscription(file: StaticString = #file,
|
||||
line: UInt = #line) -> Never {
|
||||
fatalError("Unexpected state: request before subscription sent",
|
||||
file: file,
|
||||
line: line)
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
//
|
||||
// Publishers.CollectByCount.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 24.12.2019.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Collects up to the specified number of elements, and then emits a single array of
|
||||
/// the collection.
|
||||
///
|
||||
/// If the upstream publisher finishes before filling the buffer, this publisher sends
|
||||
/// an array of all the items it has received. This may be fewer than `count`
|
||||
/// elements.
|
||||
/// If the upstream publisher fails with an error, this publisher forwards the error
|
||||
/// to the downstream receiver instead of sending its output.
|
||||
/// Note: When this publisher receives a request for `.max(n)` elements, it requests
|
||||
/// `.max(count * n)` from the upstream publisher.
|
||||
///
|
||||
/// - Parameter count: The maximum number of received elements to buffer before
|
||||
/// publishing.
|
||||
/// - Returns: A publisher that collects up to the specified number of elements, and
|
||||
/// then publishes them as an array.
|
||||
public func collect(_ count: Int) -> Publishers.CollectByCount<Self> {
|
||||
return .init(upstream: self, count: count)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that buffers a maximum number of items.
|
||||
public struct CollectByCount<Upstream: Publisher>: Publisher {
|
||||
|
||||
public typealias Output = [Upstream.Output]
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The maximum number of received elements to buffer before publishing.
|
||||
public let count: Int
|
||||
|
||||
public init(upstream: Upstream, count: Int) {
|
||||
self.upstream = upstream
|
||||
self.count = count
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Failure == Failure, Downstream.Input == Output
|
||||
{
|
||||
upstream.subscribe(Inner(downstream: subscriber, count: count))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.CollectByCount: Equatable where Upstream: Equatable {}
|
||||
|
||||
extension Publishers.CollectByCount {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == [Upstream.Output],
|
||||
Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
private let downstream: Downstream
|
||||
|
||||
private let count: Int
|
||||
|
||||
private var buffer: [Input] = []
|
||||
|
||||
private var subscription: Subscription?
|
||||
|
||||
private var finished = false
|
||||
|
||||
private let lock = UnfairLock.allocate()
|
||||
|
||||
init(downstream: Downstream, count: Int) {
|
||||
self.downstream = downstream
|
||||
self.count = count
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
if finished || self.subscription != nil {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
self.subscription = subscription
|
||||
lock.unlock()
|
||||
downstream.receive(subscription: self)
|
||||
}
|
||||
|
||||
func receive(_ input: Upstream.Output) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
if subscription == nil {
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
buffer.append(input)
|
||||
guard buffer.count == count else {
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
let output = self.buffer
|
||||
self.buffer = []
|
||||
lock.unlock()
|
||||
return downstream.receive(output) * count
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Upstream.Failure>) {
|
||||
lock.lock()
|
||||
subscription = nil
|
||||
finished = true
|
||||
switch completion {
|
||||
case .finished:
|
||||
if buffer.isEmpty {
|
||||
lock.unlock()
|
||||
} else {
|
||||
let buffer = self.buffer
|
||||
self.buffer = []
|
||||
lock.unlock()
|
||||
_ = downstream.receive(buffer)
|
||||
}
|
||||
case .failure:
|
||||
buffer = []
|
||||
lock.unlock()
|
||||
}
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
demand.assertNonZero()
|
||||
lock.lock()
|
||||
if let subscription = self.subscription {
|
||||
lock.unlock()
|
||||
subscription.request(demand * count)
|
||||
} else {
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
if let subscription = self.subscription {
|
||||
buffer = []
|
||||
finished = true
|
||||
self.subscription = nil
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
} else {
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
var description: String { return "CollectByCount" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
let children: [Mirror.Child] = [
|
||||
("downstream", downstream),
|
||||
("upstreamSubscription", subscription as Any),
|
||||
("buffer", buffer),
|
||||
("count", count)
|
||||
]
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
//
|
||||
// Publishers.Concatenate.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 24.10.2019.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Prefixes a `Publisher`'s output with the specified sequence.
|
||||
///
|
||||
/// - Parameter elements: The elements to publish before this publisher’s elements.
|
||||
/// - Returns: A publisher that prefixes the specified elements prior to this
|
||||
/// publisher’s elements.
|
||||
public func prepend(
|
||||
_ elements: Output...
|
||||
) -> Publishers.Concatenate<Publishers.Sequence<[Output], Failure>, Self> {
|
||||
return prepend(elements)
|
||||
}
|
||||
|
||||
/// Prefixes a `Publisher`'s output with the specified sequence.
|
||||
///
|
||||
/// - Parameter elements: A sequence of elements to publish before this publisher’s
|
||||
/// elements.
|
||||
/// - Returns: A publisher that prefixes the sequence of elements prior to this
|
||||
/// publisher’s elements.
|
||||
public func prepend<Elements: Sequence>(
|
||||
_ elements: Elements
|
||||
) -> Publishers.Concatenate<Publishers.Sequence<Elements, Failure>, Self>
|
||||
where Output == Elements.Element
|
||||
{
|
||||
return prepend(.init(sequence: elements))
|
||||
}
|
||||
|
||||
/// Prefixes this publisher’s output with the elements emitted by the given publisher.
|
||||
///
|
||||
/// The resulting publisher doesn’t emit any elements until the prefixing publisher
|
||||
/// finishes.
|
||||
///
|
||||
/// - Parameter publisher: The prefixing publisher.
|
||||
/// - Returns: A publisher that prefixes the prefixing publisher’s elements prior to
|
||||
/// this publisher’s elements.
|
||||
public func prepend<Prefix: Publisher>(
|
||||
_ publisher: Prefix
|
||||
) -> Publishers.Concatenate<Prefix, Self>
|
||||
where Failure == Prefix.Failure, Output == Prefix.Output
|
||||
{
|
||||
return .init(prefix: publisher, suffix: self)
|
||||
}
|
||||
|
||||
/// Append a `Publisher`'s output with the specified sequence.
|
||||
public func append(
|
||||
_ elements: Output...
|
||||
) -> Publishers.Concatenate<Self, Publishers.Sequence<[Output], Failure>> {
|
||||
return append(elements)
|
||||
}
|
||||
|
||||
/// Appends a `Publisher`'s output with the specified sequence.
|
||||
public func append<Elements: Sequence>(
|
||||
_ elements: Elements
|
||||
) -> Publishers.Concatenate<Self, Publishers.Sequence<Elements, Failure>>
|
||||
where Output == Elements.Element
|
||||
{
|
||||
return append(.init(sequence: elements))
|
||||
}
|
||||
|
||||
/// Appends this publisher’s output with the elements emitted by the given publisher.
|
||||
///
|
||||
/// This operator produces no elements until this publisher finishes. It then produces
|
||||
/// this publisher’s elements, followed by the given publisher’s elements.
|
||||
/// If this publisher fails with an error, the prefixing publisher does not publish
|
||||
/// the provided publisher’s elements.
|
||||
///
|
||||
/// - Parameter publisher: The appending publisher.
|
||||
/// - Returns: A publisher that appends the appending publisher’s elements after this
|
||||
/// publisher’s elements.
|
||||
public func append<Suffix: Publisher>(
|
||||
_ publisher: Suffix
|
||||
) -> Publishers.Concatenate<Self, Suffix>
|
||||
where Suffix.Failure == Failure, Suffix.Output == Output
|
||||
{
|
||||
return .init(prefix: self, suffix: publisher)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that emits all of one publisher’s elements before those from anothe
|
||||
/// publisher.
|
||||
public struct Concatenate<Prefix: Publisher, Suffix: Publisher>: Publisher
|
||||
where Prefix.Failure == Suffix.Failure, Prefix.Output == Suffix.Output
|
||||
{
|
||||
public typealias Output = Suffix.Output
|
||||
|
||||
public typealias Failure = Suffix.Failure
|
||||
|
||||
/// The publisher to republish, in its entirety, before republishing elements from
|
||||
/// `suffix`.
|
||||
public let prefix: Prefix
|
||||
|
||||
/// The publisher to republish only after `prefix` finishes.
|
||||
public let suffix: Suffix
|
||||
|
||||
public init(prefix: Prefix, suffix: Suffix) {
|
||||
self.prefix = prefix
|
||||
self.suffix = suffix
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Suffix.Failure == Downstream.Failure, Suffix.Output == Downstream.Input
|
||||
{
|
||||
let inner = Inner(downstream: subscriber, suffix: suffix)
|
||||
subscriber.receive(subscription: inner)
|
||||
prefix.subscribe(inner)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.Concatenate: Equatable where Prefix: Equatable, Suffix: Equatable {}
|
||||
|
||||
extension Publishers.Concatenate {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Suffix.Output, Downstream.Failure == Suffix.Failure
|
||||
{
|
||||
typealias Input = Suffix.Output
|
||||
|
||||
typealias Failure = Suffix.Failure
|
||||
|
||||
private let downstream: Downstream
|
||||
|
||||
private let suffix: Suffix
|
||||
|
||||
private var prefixFinished = false
|
||||
|
||||
private var demand = Subscribers.Demand.none
|
||||
|
||||
private var upstream: Subscription?
|
||||
|
||||
private var expectedSubscriptions = 2
|
||||
|
||||
private let lock = UnfairLock.allocate()
|
||||
|
||||
private let downstreamLock = UnfairRecursiveLock.allocate()
|
||||
|
||||
fileprivate init(downstream: Downstream, suffix: Suffix) {
|
||||
self.downstream = downstream
|
||||
self.suffix = suffix
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
downstreamLock.deallocate()
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard upstream == nil, expectedSubscriptions > 0 else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
upstream = subscription
|
||||
expectedSubscriptions -= 1
|
||||
let demand = self.demand
|
||||
lock.unlock()
|
||||
if demand > 0 {
|
||||
subscription.request(demand)
|
||||
}
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
demand -= 1
|
||||
lock.unlock()
|
||||
downstreamLock.lock()
|
||||
let newDemand = downstream.receive(input)
|
||||
downstreamLock.unlock()
|
||||
lock.lock()
|
||||
demand += newDemand
|
||||
lock.unlock()
|
||||
return newDemand
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
// Reading prefixFinished should be locked. Combine doesn't lock here.
|
||||
if prefixFinished {
|
||||
downstreamLock.lock()
|
||||
downstream.receive(completion: completion)
|
||||
downstreamLock.unlock()
|
||||
return
|
||||
}
|
||||
|
||||
guard case .finished = completion else {
|
||||
downstreamLock.lock()
|
||||
downstream.receive(completion: completion)
|
||||
downstreamLock.unlock()
|
||||
return
|
||||
}
|
||||
|
||||
prefixFinished = true // Should be locked as well?
|
||||
lock.lock()
|
||||
upstream = nil
|
||||
lock.unlock()
|
||||
suffix.subscribe(self)
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
lock.lock()
|
||||
self.demand += demand
|
||||
guard let subscription = upstream else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
lock.unlock()
|
||||
subscription.request(demand)
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
guard let subscription = upstream else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
upstream = nil
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
}
|
||||
|
||||
var description: String { return "Concatenate" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
let children: [Mirror.Child] = [
|
||||
("downstream", downstream),
|
||||
("upstreamSubscription", upstream as Any),
|
||||
("suffix", suffix),
|
||||
("demand", demand)
|
||||
]
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
//
|
||||
// Publishers.Delay.swift
|
||||
// OpenCombine
|
||||
//
|
||||
// Created by Евгений Богомолов on 07/09/2019.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Delays delivery of all output to the downstream receiver by a specified amount
|
||||
/// of time on a particular scheduler.
|
||||
///
|
||||
/// The delay affects the delivery of elements and completion, but not of the original
|
||||
/// subscription.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - interval: The amount of time to delay.
|
||||
/// - tolerance: The allowed tolerance in firing delayed events.
|
||||
/// - scheduler: The scheduler to deliver the delayed events.
|
||||
/// - Returns: A publisher that delays delivery of elements and completion to
|
||||
/// the downstream receiver.
|
||||
public func delay<Context: Scheduler>(
|
||||
for interval: Context.SchedulerTimeType.Stride,
|
||||
tolerance: Context.SchedulerTimeType.Stride? = nil,
|
||||
scheduler: Context,
|
||||
options: Context.SchedulerOptions? = nil
|
||||
) -> Publishers.Delay<Self, Context> {
|
||||
return .init(upstream: self,
|
||||
interval: interval,
|
||||
tolerance: tolerance ?? scheduler.minimumTolerance,
|
||||
scheduler: scheduler,
|
||||
options: options)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that delays delivery of elements and completion
|
||||
/// to the downstream receiver.
|
||||
public struct Delay<Upstream: Publisher, Context: Scheduler>: Publisher {
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher that this publisher receives elements from.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The amount of time to delay.
|
||||
public let interval: Context.SchedulerTimeType.Stride
|
||||
|
||||
/// The allowed tolerance in firing delayed events.
|
||||
public let tolerance: Context.SchedulerTimeType.Stride
|
||||
|
||||
/// The scheduler to deliver the delayed events.
|
||||
public let scheduler: Context
|
||||
|
||||
public let options: Context.SchedulerOptions?
|
||||
|
||||
public init(upstream: Upstream,
|
||||
interval: Context.SchedulerTimeType.Stride,
|
||||
tolerance: Context.SchedulerTimeType.Stride,
|
||||
scheduler: Context,
|
||||
options: Context.SchedulerOptions? = nil)
|
||||
{
|
||||
self.upstream = upstream
|
||||
self.interval = interval
|
||||
self.tolerance = tolerance
|
||||
self.scheduler = scheduler
|
||||
self.options = options
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Failure == Downstream.Failure,
|
||||
Upstream.Output == Downstream.Input
|
||||
{
|
||||
upstream.subscribe(Inner(self, downstream: subscriber))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.Delay {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
Subscription
|
||||
where Downstream.Input == Upstream.Output, Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
// NOTE: This class has been audited for thread safety
|
||||
|
||||
typealias Input = Upstream.Output
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
fileprivate typealias Delay = Publishers.Delay<Upstream, Context>
|
||||
|
||||
private enum State {
|
||||
case ready(Delay, Downstream)
|
||||
case subscribed(Delay, Downstream, Subscription)
|
||||
case terminal
|
||||
}
|
||||
|
||||
private let lock = UnfairLock.allocate()
|
||||
private var state: State
|
||||
private let downstreamLock = UnfairRecursiveLock.allocate()
|
||||
|
||||
fileprivate init(_ publisher: Delay, downstream: Downstream) {
|
||||
state = .ready(publisher, downstream)
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
downstreamLock.deallocate()
|
||||
}
|
||||
|
||||
private func schedule(_ delay: Delay, work: @escaping () -> Void) {
|
||||
delay
|
||||
.scheduler
|
||||
.schedule(after: delay.scheduler.now.advanced(by: delay.interval),
|
||||
tolerance: delay.tolerance,
|
||||
options: delay.options,
|
||||
work)
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard case let .ready(delay, downstream) = state else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
state = .subscribed(delay, downstream, subscription)
|
||||
lock.unlock()
|
||||
downstreamLock.lock()
|
||||
downstream.receive(subscription: self)
|
||||
downstreamLock.unlock()
|
||||
}
|
||||
|
||||
func receive(_ input: Upstream.Output) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
guard case let .subscribed(delay, downstream, _) = state else {
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
lock.unlock()
|
||||
schedule(delay) {
|
||||
self.scheduledReceive(input, downstream: downstream)
|
||||
}
|
||||
return .none
|
||||
}
|
||||
|
||||
private func scheduledReceive(_ input: Upstream.Output, downstream: Downstream) {
|
||||
downstreamLock.lock()
|
||||
let newDemand = downstream.receive(input)
|
||||
downstreamLock.unlock()
|
||||
guard newDemand > 0 else {
|
||||
return
|
||||
}
|
||||
lock.lock()
|
||||
guard case let .subscribed(_, _, subscription) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
lock.unlock()
|
||||
subscription.request(newDemand)
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
lock.lock()
|
||||
guard case let .subscribed(delay, downstream, _) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
state = .terminal
|
||||
lock.unlock()
|
||||
schedule(delay) {
|
||||
self.scheduledReceive(completion: completion, downstream: downstream)
|
||||
}
|
||||
}
|
||||
|
||||
private func scheduledReceive(completion: Subscribers.Completion<Failure>,
|
||||
downstream: Downstream) {
|
||||
downstreamLock.lock()
|
||||
downstream.receive(completion: completion)
|
||||
downstreamLock.unlock()
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
lock.lock()
|
||||
guard case let .subscribed(_, _, subscription) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
lock.unlock()
|
||||
subscription.request(demand)
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
guard case let .subscribed(_, _, subscription) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
state = .terminal
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,6 @@
|
||||
// Created by Sven Weidauer on 03.10.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
extension Publisher {
|
||||
/// Omits the specified number of elements before republishing subsequent elements.
|
||||
///
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
//
|
||||
// Publishers.DropUntilOutput.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 24.12.2019.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Ignores elements from the upstream publisher until it receives an element from
|
||||
/// a second publisher.
|
||||
///
|
||||
/// This publisher requests a single value from the upstream publisher, and it ignores
|
||||
/// (drops) all elements from that publisher until the upstream publisher produces
|
||||
/// a value. After the `other` publisher produces an element, this publisher cancels
|
||||
/// its subscription to the `other` publisher, and allows events from the `upstream`
|
||||
/// publisher to pass through.
|
||||
/// After this publisher receives a subscription from the upstream publisher, it
|
||||
/// passes through backpressure requests from downstream to the upstream publisher.
|
||||
/// If the upstream publisher acts on those requests before the other publisher
|
||||
/// produces an item, this publisher drops the elements it receives from the upstream
|
||||
/// publisher.
|
||||
///
|
||||
/// - Parameter publisher: A publisher to monitor for its first emitted element.
|
||||
/// - Returns: A publisher that drops elements from the upstream publisher until the
|
||||
/// `other` publisher produces a value.
|
||||
public func drop<Other: Publisher>(
|
||||
untilOutputFrom publisher: Other
|
||||
) -> Publishers.DropUntilOutput<Self, Other> where Failure == Other.Failure {
|
||||
return .init(upstream: self, other: publisher)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that ignores elements from the upstream publisher until it receives
|
||||
/// an element from second publisher.
|
||||
public struct DropUntilOutput<Upstream: Publisher, Other: Publisher>: Publisher
|
||||
where Upstream.Failure == Other.Failure
|
||||
{
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher that this publisher receives elements from.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// A publisher to monitor for its first emitted element.
|
||||
public let other: Other
|
||||
|
||||
/// Creates a publisher that ignores elements from the upstream publisher until
|
||||
/// it receives an element from another publisher.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - upstream: A publisher to drop elements from while waiting for another
|
||||
/// publisher to emit elements.
|
||||
/// - other: A publisher to monitor for its first emitted element.
|
||||
public init(upstream: Upstream, other: Other) {
|
||||
self.upstream = upstream
|
||||
self.other = other
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Output == Downstream.Input,
|
||||
Other.Failure == Downstream.Failure
|
||||
{
|
||||
let inner = Inner(downstream: subscriber)
|
||||
subscriber.receive(subscription: inner)
|
||||
other.subscribe(Inner.OtherSubscriber(inner: inner))
|
||||
upstream.subscribe(inner)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.DropUntilOutput: Equatable
|
||||
where Upstream: Equatable, Other: Equatable {}
|
||||
|
||||
extension Publishers.DropUntilOutput {
|
||||
fileprivate final class Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Upstream.Output, Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
private let downstream: Downstream
|
||||
|
||||
private var triggered = false
|
||||
|
||||
private let lock = UnfairLock.allocate()
|
||||
|
||||
private let downstreamLock = UnfairRecursiveLock.allocate()
|
||||
|
||||
private var upstreamSubscription: Subscription?
|
||||
|
||||
private var pendingDemand = Subscribers.Demand.none
|
||||
|
||||
private var otherSubscription: Subscription?
|
||||
|
||||
private var otherFinished = false
|
||||
|
||||
private var cancelled = false
|
||||
|
||||
init(downstream: Downstream) {
|
||||
self.downstream = downstream
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
downstreamLock.deallocate()
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard upstreamSubscription == nil && !cancelled else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
upstreamSubscription = subscription
|
||||
if pendingDemand > 0 {
|
||||
lock.unlock()
|
||||
subscription.request(pendingDemand)
|
||||
} else {
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
if !triggered || cancelled {
|
||||
pendingDemand -= 1
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
lock.unlock()
|
||||
downstreamLock.lock()
|
||||
let newDemand = downstream.receive(input)
|
||||
downstreamLock.unlock()
|
||||
return newDemand
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
lock.lock()
|
||||
if cancelled {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
cancelled = true
|
||||
lock.unlock()
|
||||
downstreamLock.lock()
|
||||
downstream.receive(completion: completion)
|
||||
downstreamLock.unlock()
|
||||
}
|
||||
|
||||
private func receiveOther(subscription: Subscription) {
|
||||
// Combine doesn't lock here
|
||||
guard otherSubscription == nil else {
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
otherSubscription = subscription
|
||||
subscription.request(.max(1))
|
||||
}
|
||||
|
||||
private func receiveOther(_ input: Other.Output) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
triggered = true
|
||||
otherSubscription = nil
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
|
||||
private func receiveOther(completion: Subscribers.Completion<Other.Failure>) {
|
||||
lock.lock()
|
||||
if triggered {
|
||||
otherSubscription = nil
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
|
||||
otherFinished = true
|
||||
if let upstreamSubscription = self.upstreamSubscription {
|
||||
self.upstreamSubscription = nil
|
||||
lock.unlock()
|
||||
upstreamSubscription.cancel()
|
||||
} else {
|
||||
lock.unlock()
|
||||
}
|
||||
downstreamLock.lock()
|
||||
downstream.receive(completion: completion)
|
||||
downstreamLock.unlock()
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
lock.lock()
|
||||
pendingDemand += demand
|
||||
if let subscription = upstreamSubscription {
|
||||
lock.unlock()
|
||||
subscription.request(demand)
|
||||
} else {
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
let upstreamSubscription = self.upstreamSubscription
|
||||
let otherSubscription = self.otherSubscription
|
||||
self.upstreamSubscription = nil
|
||||
self.otherSubscription = nil
|
||||
cancelled = true
|
||||
lock.unlock()
|
||||
|
||||
upstreamSubscription?.cancel()
|
||||
otherSubscription?.cancel()
|
||||
}
|
||||
|
||||
var description: String { return "DropUntilOutput" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
return Mirror(self, children: EmptyCollection())
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.DropUntilOutput.Inner {
|
||||
fileprivate struct OtherSubscriber
|
||||
: Subscriber,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
{
|
||||
let inner: Publishers.DropUntilOutput<Upstream, Other>.Inner<Downstream>
|
||||
|
||||
var combineIdentifier: CombineIdentifier {
|
||||
return inner.combineIdentifier
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
inner.receiveOther(subscription: subscription)
|
||||
}
|
||||
|
||||
func receive(_ input: Other.Output) -> Subscribers.Demand {
|
||||
return inner.receiveOther(input)
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Other.Failure>) {
|
||||
inner.receiveOther(completion: completion)
|
||||
}
|
||||
|
||||
var description: String { return "DropUntilOutput" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
return Mirror(self, children: EmptyCollection())
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,6 @@
|
||||
// Created by Sergej Jaskiewicz on 16.06.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Omits elements from the upstream publisher until a given closure returns false,
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
// Created by Eric Patey on 16.08.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
extension Publisher {
|
||||
/// Transforms all elements from an upstream publisher into a new or existing
|
||||
/// publisher.
|
||||
@@ -80,9 +78,8 @@ extension Publishers.FlatMap {
|
||||
/// acquired.
|
||||
private var outerSubscription: Subscription?
|
||||
|
||||
// Must be recursive lock. Probably a bug in Combine.
|
||||
/// The lock for requesting from `outerSubscription`.
|
||||
private let outerLock = UnfairLock.allocate()
|
||||
private let outerLock = UnfairRecursiveLock.allocate()
|
||||
|
||||
/// The lock for modifying the state. All mutable state here should be
|
||||
/// read and modified with this lock acquired.
|
||||
@@ -90,10 +87,9 @@ extension Publishers.FlatMap {
|
||||
/// by the `downstreamLock`.
|
||||
private let lock = UnfairLock.allocate()
|
||||
|
||||
// Must be recursive lock. Probably a bug in Combine.
|
||||
/// All the calls to the downstream subscriber should be made with this lock
|
||||
/// acquired.
|
||||
private let downstreamLock = UnfairLock.allocate()
|
||||
private let downstreamLock = UnfairRecursiveLock.allocate()
|
||||
|
||||
private let downstream: Downstream
|
||||
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
//
|
||||
// Publishers.HandleEvents.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 03.12.2019.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Performs the specified closures when publisher events occur.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - receiveSubscription: A closure that executes when the publisher receives
|
||||
/// the subscription from the upstream publisher. Defaults to `nil`.
|
||||
/// - receiveOutput: A closure that executes when the publisher receives a value
|
||||
/// from the upstream publisher. Defaults to `nil`.
|
||||
/// - receiveCompletion: A closure that executes when the publisher receives
|
||||
/// the completion from the upstream publisher. Defaults to `nil`.
|
||||
/// - receiveCancel: A closure that executes when the downstream receiver cancels
|
||||
/// publishing. Defaults to `nil`.
|
||||
/// - receiveRequest: A closure that executes when the publisher receives a request
|
||||
/// for more elements. Defaults to `nil`.
|
||||
/// - Returns: A publisher that performs the specified closures when publisher events
|
||||
/// occur.
|
||||
public func handleEvents(
|
||||
receiveSubscription: ((Subscription) -> Void)? = nil,
|
||||
receiveOutput: ((Output) -> Void)? = nil,
|
||||
receiveCompletion: ((Subscribers.Completion<Failure>) -> Void)? = nil,
|
||||
receiveCancel: (() -> Void)? = nil,
|
||||
receiveRequest: ((Subscribers.Demand) -> Void)? = nil
|
||||
) -> Publishers.HandleEvents<Self> {
|
||||
return .init(upstream: self,
|
||||
receiveSubscription: receiveSubscription,
|
||||
receiveOutput: receiveOutput,
|
||||
receiveCompletion: receiveCompletion,
|
||||
receiveCancel: receiveCancel,
|
||||
receiveRequest: receiveRequest)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that performs the specified closures when publisher events occur.
|
||||
public struct HandleEvents<Upstream: Publisher>: Publisher {
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// A closure that executes when the publisher receives the subscription from
|
||||
/// the upstream publisher.
|
||||
public var receiveSubscription: ((Subscription) -> Void)?
|
||||
|
||||
/// A closure that executes when the publisher receives a value from the upstream
|
||||
/// publisher.
|
||||
public var receiveOutput: ((Upstream.Output) -> Void)?
|
||||
|
||||
/// A closure that executes when the publisher receives the completion from
|
||||
/// the upstream publisher.
|
||||
public var receiveCompletion:
|
||||
((Subscribers.Completion<Upstream.Failure>) -> Void)?
|
||||
|
||||
/// A closure that executes when the downstream receiver cancels publishing.
|
||||
public var receiveCancel: (() -> Void)?
|
||||
|
||||
/// A closure that executes when the publisher receives a request for more
|
||||
/// elements.
|
||||
public var receiveRequest: ((Subscribers.Demand) -> Void)?
|
||||
|
||||
public init(
|
||||
upstream: Upstream,
|
||||
receiveSubscription: ((Subscription) -> Void)? = nil,
|
||||
receiveOutput: ((Output) -> Void)? = nil,
|
||||
receiveCompletion: ((Subscribers.Completion<Failure>) -> Void)? = nil,
|
||||
receiveCancel: (() -> Void)? = nil,
|
||||
receiveRequest: ((Subscribers.Demand) -> Void)?
|
||||
) {
|
||||
self.upstream = upstream
|
||||
self.receiveSubscription = receiveSubscription
|
||||
self.receiveOutput = receiveOutput
|
||||
self.receiveCompletion = receiveCompletion
|
||||
self.receiveCancel = receiveCancel
|
||||
self.receiveRequest = receiveRequest
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Failure == Downstream.Failure,
|
||||
Upstream.Output == Downstream.Input
|
||||
{
|
||||
let inner = Inner(self, downstream: subscriber)
|
||||
subscriber.receive(subscription: inner)
|
||||
upstream.subscribe(inner)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.HandleEvents {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Upstream.Output, Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
private var status = SubscriptionStatus.awaitingSubscription
|
||||
private var pendingDemand = Subscribers.Demand.none
|
||||
private let lock = UnfairLock.allocate()
|
||||
private var events: Publishers.HandleEvents<Upstream>?
|
||||
private let downstream: Downstream
|
||||
|
||||
init(_ events: Publishers.HandleEvents<Upstream>, downstream: Downstream) {
|
||||
self.events = events
|
||||
self.downstream = downstream
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
events?.receiveSubscription?(subscription)
|
||||
lock.lock()
|
||||
guard case .awaitingSubscription = status else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
status = .subscribed(subscription)
|
||||
let pendingDemand = self.pendingDemand
|
||||
self.pendingDemand = .none
|
||||
lock.unlock()
|
||||
if pendingDemand > 0 {
|
||||
subscription.request(pendingDemand)
|
||||
}
|
||||
}
|
||||
|
||||
func receive(_ input: Upstream.Output) -> Subscribers.Demand {
|
||||
events?.receiveOutput?(input)
|
||||
let newDemand = downstream.receive(input)
|
||||
if newDemand > 0 {
|
||||
events?.receiveRequest?(newDemand)
|
||||
}
|
||||
return newDemand
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Upstream.Failure>) {
|
||||
events?.receiveCompletion?(completion)
|
||||
lock.lock()
|
||||
events = nil
|
||||
status = .terminal
|
||||
lock.unlock()
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
events?.receiveRequest?(demand)
|
||||
lock.lock()
|
||||
if case let .subscribed(subscription) = status {
|
||||
lock.unlock()
|
||||
subscription.request(demand)
|
||||
return
|
||||
}
|
||||
pendingDemand += demand
|
||||
lock.unlock()
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
events?.receiveCancel?()
|
||||
lock.lock()
|
||||
guard case let .subscribed(subscription) = status else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
events = nil
|
||||
status = .terminal
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
}
|
||||
|
||||
var description: String { return "HandleEvents" }
|
||||
|
||||
var customMirror: Mirror { return Mirror(self, children: EmptyCollection()) }
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,6 @@
|
||||
// Created by Eric Patey on 16.08.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Ingores all upstream elements, but passes along a completion
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
// Created by Anton Nazarov on 25.06.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Transforms all elements from the upstream publisher with a provided closure.
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
//
|
||||
// Publishers.MeasureInterval.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 03.12.2019.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Measures and emits the time interval between events received from an upstream
|
||||
/// publisher.
|
||||
///
|
||||
/// The output type of the returned scheduler is the time interval of the provided
|
||||
/// scheduler.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - scheduler: The scheduler on which to deliver elements.
|
||||
/// - options: Options that customize the delivery of elements.
|
||||
/// - Returns: A publisher that emits elements representing the time interval between
|
||||
/// the elements it receives.
|
||||
public func measureInterval<Context: Scheduler>(
|
||||
using scheduler: Context,
|
||||
options: Context.SchedulerOptions? = nil
|
||||
) -> Publishers.MeasureInterval<Self, Context> {
|
||||
return .init(upstream: self, scheduler: scheduler)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that measures and emits the time interval between events received from
|
||||
/// an upstream publisher.
|
||||
public struct MeasureInterval<Upstream: Publisher, Context: Scheduler>: Publisher {
|
||||
|
||||
public typealias Output = Context.SchedulerTimeType.Stride
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The scheduler on which to deliver elements.
|
||||
public let scheduler: Context
|
||||
|
||||
public init(upstream: Upstream, scheduler: Context) {
|
||||
self.upstream = upstream
|
||||
self.scheduler = scheduler
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Failure == Downstream.Failure,
|
||||
Downstream.Input == Context.SchedulerTimeType.Stride
|
||||
{
|
||||
upstream.subscribe(Inner(self, downstream: subscriber))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.MeasureInterval {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Context.SchedulerTimeType.Stride,
|
||||
Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
// NOTE: This class has been audited for thread safety
|
||||
|
||||
typealias Input = Upstream.Output
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
typealias MeasureInterval = Publishers.MeasureInterval<Upstream, Context>
|
||||
|
||||
private enum State {
|
||||
case ready(MeasureInterval, Downstream)
|
||||
case subscribed(MeasureInterval, Downstream, Subscription)
|
||||
case terminal
|
||||
}
|
||||
|
||||
private let lock = UnfairLock.allocate()
|
||||
|
||||
private var state: State
|
||||
|
||||
private var last: Context.SchedulerTimeType?
|
||||
|
||||
init(_ measureInterval: MeasureInterval, downstream: Downstream) {
|
||||
state = .ready(measureInterval, downstream)
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard case let .ready(measureInterval, downstream) = state else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
state = .subscribed(measureInterval, downstream, subscription)
|
||||
last = measureInterval.scheduler.now
|
||||
lock.unlock()
|
||||
downstream.receive(subscription: self)
|
||||
}
|
||||
|
||||
func receive(_: Input) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
guard case let .subscribed(measureInterval, downstream, subscription) = state,
|
||||
let previousTime = last else {
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
let now = measureInterval.scheduler.now
|
||||
last = now
|
||||
lock.unlock()
|
||||
let newDemand = downstream.receive(previousTime.distance(to: now))
|
||||
if newDemand > 0 {
|
||||
subscription.request(newDemand)
|
||||
}
|
||||
return .none
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
lock.lock()
|
||||
guard case let .subscribed(_, downstream, _) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
state = .terminal
|
||||
last = nil
|
||||
lock.unlock()
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
lock.lock()
|
||||
guard case let .subscribed(_, _, subscription) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
lock.unlock()
|
||||
subscription.request(demand)
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
guard case let .subscribed(_, _, subscription) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
state = .terminal
|
||||
last = nil
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
}
|
||||
|
||||
var description: String { return "MeasureInterval" }
|
||||
|
||||
var customMirror: Mirror { return Mirror(self, children: EmptyCollection()) }
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,6 @@
|
||||
// Created by Sergej Jaskiewicz on 14.06.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Applies a closure to create a subject that delivers elements to subscribers.
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
// Created by Sergej Jaskiewicz on 24.10.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Republishes elements up to the specified maximum count.
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
// Created by Sergej Jaskiewicz on 16.06.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Prints log messages for all publishing events.
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
//
|
||||
// Publishers.ReceiveOn.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 02.12.2019.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
/// Specifies the scheduler on which to receive elements from the publisher.
|
||||
///
|
||||
/// You use the `receive(on:options:)` operator to receive results on a specific
|
||||
/// scheduler, such as performing UI work on the main run loop.
|
||||
/// In contrast with `subscribe(on:options:)`, which affects upstream messages,
|
||||
/// `receive(on:options:)` changes the execution context of downstream messages.
|
||||
/// In the following example, requests to `jsonPublisher` are performed on
|
||||
/// `backgroundQueue`, but elements received from it are performed on `RunLoop.main`.
|
||||
///
|
||||
/// // Some publisher.
|
||||
/// let jsonPublisher = MyJSONLoaderPublisher()
|
||||
///
|
||||
/// // Some subscriber that updates the UI.
|
||||
/// let labelUpdater = MyLabelUpdateSubscriber()
|
||||
///
|
||||
/// jsonPublisher
|
||||
/// .subscribe(on: backgroundQueue)
|
||||
/// .receiveOn(on: RunLoop.main)
|
||||
/// .subscribe(labelUpdater)
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - scheduler: The scheduler the publisher is to use for element delivery.
|
||||
/// - options: Scheduler options that customize the element delivery.
|
||||
/// - Returns: A publisher that delivers elements using the specified scheduler.
|
||||
public func receive<Context: Scheduler>(
|
||||
on scheduler: Context,
|
||||
options: Context.SchedulerOptions? = nil
|
||||
) -> Publishers.ReceiveOn<Self, Context> {
|
||||
return .init(upstream: self, scheduler: scheduler, options: options)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that delivers elements to its downstream subscriber on a specific
|
||||
/// scheduler.
|
||||
public struct ReceiveOn<Upstream: Publisher, Context: Scheduler>: Publisher {
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The scheduler the publisher is to use for element delivery.
|
||||
public let scheduler: Context
|
||||
|
||||
/// Scheduler options that customize the delivery of elements.
|
||||
public let options: Context.SchedulerOptions?
|
||||
|
||||
public init(upstream: Upstream,
|
||||
scheduler: Context,
|
||||
options: Context.SchedulerOptions?) {
|
||||
self.upstream = upstream
|
||||
self.scheduler = scheduler
|
||||
self.options = options
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Failure == Downstream.Failure,
|
||||
Upstream.Output == Downstream.Input
|
||||
{
|
||||
upstream.subscribe(Inner(self, downstream: subscriber))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.ReceiveOn {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Upstream.Output, Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
typealias ReceiveOn = Publishers.ReceiveOn<Upstream, Context>
|
||||
|
||||
private enum State {
|
||||
case ready(ReceiveOn, Downstream)
|
||||
case subscribed(ReceiveOn, Downstream, Subscription)
|
||||
case terminal
|
||||
}
|
||||
|
||||
private let lock = UnfairLock.allocate()
|
||||
private var state: State
|
||||
private let downstreamLock = UnfairRecursiveLock.allocate()
|
||||
|
||||
init(_ receiveOn: ReceiveOn, downstream: Downstream) {
|
||||
state = .ready(receiveOn, downstream)
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
downstreamLock.deallocate()
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard case let .ready(receiveOn, downstream) = state else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
state = .subscribed(receiveOn, downstream, subscription)
|
||||
lock.unlock()
|
||||
downstreamLock.lock()
|
||||
downstream.receive(subscription: self)
|
||||
downstreamLock.unlock()
|
||||
}
|
||||
|
||||
func receive(_ input: Upstream.Output) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
guard case let .subscribed(receiveOn, downstream, _) = state else {
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
lock.unlock()
|
||||
receiveOn.scheduler.schedule(options: receiveOn.options) {
|
||||
self.scheduledReceive(input, downstream: downstream)
|
||||
}
|
||||
return .none
|
||||
}
|
||||
|
||||
private func scheduledReceive(_ input: Upstream.Output, downstream: Downstream) {
|
||||
downstreamLock.lock()
|
||||
let newDemand = downstream.receive(input)
|
||||
downstreamLock.unlock()
|
||||
guard newDemand > 0 else {
|
||||
return
|
||||
}
|
||||
lock.lock()
|
||||
guard case let .subscribed(_, _, subscription) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
lock.unlock()
|
||||
subscription.request(newDemand)
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Upstream.Failure>) {
|
||||
lock.lock()
|
||||
guard case let .subscribed(receiveOn, downstream, _) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
state = .terminal
|
||||
lock.unlock()
|
||||
receiveOn.scheduler.schedule(options: receiveOn.options) {
|
||||
self.scheduledReceive(completion: completion, downstream: downstream)
|
||||
}
|
||||
}
|
||||
|
||||
private func scheduledReceive(completion: Subscribers.Completion<Failure>,
|
||||
downstream: Downstream) {
|
||||
downstreamLock.lock()
|
||||
downstream.receive(completion: completion)
|
||||
downstreamLock.unlock()
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
lock.lock()
|
||||
guard case let .subscribed(_, _, subscription) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
lock.unlock()
|
||||
subscription.request(demand)
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
guard case let .subscribed(_, _, subscription) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
state = .terminal
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
}
|
||||
|
||||
var description: String { return "ReceiveOn" }
|
||||
|
||||
var customMirror: Mirror { return Mirror(self, children: EmptyCollection()) }
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
//
|
||||
// Publishers.ReplaceEmpty.swift
|
||||
// OpenCombine
|
||||
//
|
||||
// Created by Joe Spadafora on 12/10/19.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Replaces an empty stream with the provided element.
|
||||
///
|
||||
/// If the upstream publisher finishes without producing any elements,
|
||||
/// this publisher emits the provided element, then finishes normally.
|
||||
/// - Parameter output: An element to emit when the upstream publisher
|
||||
/// finishes without emitting any elements.
|
||||
/// - Returns: A publisher that replaces an empty stream with
|
||||
/// the provided output element.
|
||||
public func replaceEmpty(with output: Output) -> Publishers.ReplaceEmpty<Self> {
|
||||
return .init(upstream: self, output: output)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that replaces an empty stream with a provided element.
|
||||
public struct ReplaceEmpty<Upstream: Publisher>: Publisher {
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The element to deliver when the upstream publisher finishes
|
||||
/// without delivering any elements.
|
||||
public let output: Upstream.Output
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
public init(upstream: Upstream, output: Output) {
|
||||
self.upstream = upstream
|
||||
self.output = output
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Failure == Downstream.Failure,
|
||||
Upstream.Output == Downstream.Input
|
||||
{
|
||||
let inner = Inner(downstream: subscriber, output: output)
|
||||
upstream.subscribe(inner)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.ReplaceEmpty: Equatable
|
||||
where Upstream: Equatable, Upstream.Output: Equatable {}
|
||||
|
||||
extension Publishers.ReplaceEmpty {
|
||||
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Upstream.Failure == Downstream.Failure,
|
||||
Upstream.Output == Downstream.Input
|
||||
{
|
||||
|
||||
typealias Input = Upstream.Output
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
private let output: Output
|
||||
private let downstream: Downstream
|
||||
|
||||
private var receivedUpstream = false
|
||||
private var lock = UnfairLock.allocate()
|
||||
private var downstreamRequested = false
|
||||
private var finishedWithoutUpstream = false
|
||||
|
||||
private var status = SubscriptionStatus.awaitingSubscription
|
||||
|
||||
fileprivate init(downstream: Downstream, output: Output) {
|
||||
self.downstream = downstream
|
||||
self.output = output
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard case .awaitingSubscription = status else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
status = .subscribed(subscription)
|
||||
lock.unlock()
|
||||
downstream.receive(subscription: self)
|
||||
subscription.request(.unlimited)
|
||||
}
|
||||
|
||||
func receive(_ input: Upstream.Output) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
guard case .subscribed = status else {
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
receivedUpstream = true
|
||||
lock.unlock()
|
||||
return downstream.receive(input)
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Upstream.Failure>) {
|
||||
lock.lock()
|
||||
guard case .subscribed = status else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
status = .terminal
|
||||
if receivedUpstream {
|
||||
lock.unlock()
|
||||
downstream.receive(completion: completion)
|
||||
return
|
||||
}
|
||||
switch completion {
|
||||
case .finished:
|
||||
if downstreamRequested {
|
||||
lock.unlock()
|
||||
_ = downstream.receive(output)
|
||||
downstream.receive(completion: completion)
|
||||
return
|
||||
}
|
||||
finishedWithoutUpstream = true
|
||||
lock.unlock()
|
||||
case .failure:
|
||||
lock.unlock()
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
demand.assertNonZero()
|
||||
lock.lock()
|
||||
downstreamRequested = true
|
||||
if finishedWithoutUpstream {
|
||||
lock.unlock()
|
||||
_ = downstream.receive(output)
|
||||
downstream.receive(completion: .finished)
|
||||
return
|
||||
}
|
||||
guard case let .subscribed(subscription) = status else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
lock.unlock()
|
||||
subscription.request(demand)
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
guard case let .subscribed(subscription) = status else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
status = .terminal
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
}
|
||||
|
||||
var description: String { return "ReplaceEmpty" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
return Mirror(self, children: EmptyCollection())
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,6 @@
|
||||
// Created by Bogdan Vlad on 8/29/19.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
extension Publisher {
|
||||
/// Replaces any errors in the stream with the provided element.
|
||||
///
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
// Created by Eric Patey on 26.08.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Transforms elements from the upstream publisher by providing the current element
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
// Created by Sergej Jaskiewicz on 19.06.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that publishes a given sequence of elements.
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
//
|
||||
// Publishers.SubscribeOn.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 02.12.2019.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Specifies the scheduler on which to perform subscribe, cancel, and request
|
||||
/// operations.
|
||||
///
|
||||
/// In contrast with `receive(on:options:)`, which affects downstream messages,
|
||||
/// `subscribe(on:)` changes the execution context of upstream messages.
|
||||
/// In the following example, requests to `jsonPublisher` are performed on
|
||||
/// `backgroundQueue`, but elements received from it are performed on `RunLoop.main`.
|
||||
///
|
||||
/// let ioPerformingPublisher == // Some publisher.
|
||||
/// let uiUpdatingSubscriber == // Some subscriber that updates the UI.
|
||||
///
|
||||
/// ioPerformingPublisher
|
||||
/// .subscribe(on: backgroundQueue)
|
||||
/// .receiveOn(on: RunLoop.main)
|
||||
/// .subscribe(uiUpdatingSubscriber)
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - scheduler: The scheduler on which to receive upstream messages.
|
||||
/// - options: Options that customize the delivery of elements.
|
||||
/// - Returns: A publisher which performs upstream operations on the specified
|
||||
/// scheduler.
|
||||
public func subscribe<Context: Scheduler>(
|
||||
on scheduler: Context,
|
||||
options: Context.SchedulerOptions? = nil
|
||||
) -> Publishers.SubscribeOn<Self, Context> {
|
||||
return .init(upstream: self, scheduler: scheduler, options: options)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that receives elements from an upstream publisher on a specific
|
||||
/// scheduler.
|
||||
public struct SubscribeOn<Upstream: Publisher, Context: Scheduler>: Publisher {
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The scheduler the publisher should use to receive elements.
|
||||
public let scheduler: Context
|
||||
|
||||
/// Scheduler options that customize the delivery of elements.
|
||||
public let options: Context.SchedulerOptions?
|
||||
|
||||
public init(upstream: Upstream,
|
||||
scheduler: Context,
|
||||
options: Context.SchedulerOptions?) {
|
||||
self.upstream = upstream
|
||||
self.scheduler = scheduler
|
||||
self.options = options
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Failure == Downstream.Failure,
|
||||
Upstream.Output == Downstream.Input
|
||||
{
|
||||
scheduler.schedule(options: options) {
|
||||
self.upstream.subscribe(Inner(self, downstream: subscriber))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.SubscribeOn {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Upstream.Output, Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
typealias SubscribeOn = Publishers.SubscribeOn<Upstream, Context>
|
||||
|
||||
private enum State {
|
||||
case ready(SubscribeOn, Downstream)
|
||||
case subscribed(SubscribeOn, Downstream, Subscription)
|
||||
case terminal
|
||||
}
|
||||
|
||||
private let lock = UnfairLock.allocate()
|
||||
private var state: State
|
||||
private let upstreamLock = UnfairLock.allocate()
|
||||
|
||||
init(_ subscribeOn: SubscribeOn, downstream: Downstream) {
|
||||
state = .ready(subscribeOn, downstream)
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
upstreamLock.deallocate()
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard case let .ready(subscribeOn, downstream) = state else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
state = .subscribed(subscribeOn, downstream, subscription)
|
||||
lock.unlock()
|
||||
downstream.receive(subscription: self)
|
||||
}
|
||||
|
||||
func receive(_ input: Upstream.Output) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
guard case let .subscribed(_, downstream, _) = state else {
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
lock.unlock()
|
||||
return downstream.receive(input)
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Upstream.Failure>) {
|
||||
lock.lock()
|
||||
guard case let .subscribed(_, downstream, _) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
state = .terminal
|
||||
lock.unlock()
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
lock.lock()
|
||||
guard case let .subscribed(subscribeOn, _, subscription) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
lock.unlock()
|
||||
subscribeOn.scheduler.schedule(options: subscribeOn.options) { [weak self] in
|
||||
self?.scheduledRequest(demand, subscription: subscription)
|
||||
}
|
||||
}
|
||||
|
||||
private func scheduledRequest(_ demand: Subscribers.Demand,
|
||||
subscription: Subscription) {
|
||||
upstreamLock.lock()
|
||||
subscription.request(demand)
|
||||
upstreamLock.unlock()
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
guard case let .subscribed(subscribeOn, _, subscription) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
state = .terminal
|
||||
lock.unlock()
|
||||
subscribeOn.scheduler.schedule(options: subscribeOn.options) { [weak self] in
|
||||
self?.scheduledCancel(subscription)
|
||||
}
|
||||
}
|
||||
|
||||
private func scheduledCancel(_ subscription: Subscription) {
|
||||
upstreamLock.lock()
|
||||
subscription.cancel()
|
||||
upstreamLock.unlock()
|
||||
}
|
||||
|
||||
var description: String { return "SubscribeOn" }
|
||||
|
||||
var customMirror: Mirror { return Mirror(self, children: EmptyCollection()) }
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
//
|
||||
// Publishers.SwitchToLatest.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 07.01.2020.
|
||||
//
|
||||
|
||||
extension Publisher where Output: Publisher, Output.Failure == Failure {
|
||||
|
||||
/// Flattens the stream of events from multiple upstream publishers to appear as if
|
||||
/// they were coming from a single stream of events.
|
||||
///
|
||||
/// This operator switches the inner publisher as new ones arrive but keeps the outer
|
||||
/// one constant for downstream subscribers.
|
||||
/// For example, given the type `Publisher<Publisher<Data, NSError>, Never>`,
|
||||
/// calling `switchToLatest()` will result in the type `Publisher<Data, NSError>`.
|
||||
/// The downstream subscriber sees a continuous stream of values even though they may
|
||||
/// be coming from different upstream publishers.
|
||||
public func switchToLatest() -> Publishers.SwitchToLatest<Output, Self> {
|
||||
return .init(upstream: self)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that “flattens” nested publishers.
|
||||
///
|
||||
/// Given a publisher that publishes Publishers, the `SwitchToLatest` publisher
|
||||
/// produces a sequence of events from only the most recent one.
|
||||
///
|
||||
/// For example, given the type `Publisher<Publisher<Data, NSError>, Never>`,
|
||||
/// calling `switchToLatest()` will result in the type `Publisher<Data, NSError>`.
|
||||
/// The downstream subscriber sees a continuous stream of values even though they may
|
||||
/// be coming from different upstream publishers.
|
||||
public struct SwitchToLatest<NestedPublisher: Publisher, Upstream: Publisher>
|
||||
: Publisher
|
||||
where Upstream.Output == NestedPublisher,
|
||||
Upstream.Failure == NestedPublisher.Failure
|
||||
{
|
||||
public typealias Output = NestedPublisher.Output
|
||||
|
||||
public typealias Failure = NestedPublisher.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// Creates a publisher that “flattens” nested publishers.
|
||||
///
|
||||
/// - Parameter upstream: The publisher from which this publisher receives
|
||||
/// elements.
|
||||
public init(upstream: Upstream) {
|
||||
self.upstream = upstream
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Input == Output, Downstream.Failure == Failure
|
||||
{
|
||||
let outer = Outer(downstream: subscriber)
|
||||
subscriber.receive(subscription: outer)
|
||||
upstream.subscribe(outer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.SwitchToLatest {
|
||||
fileprivate final class Outer<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == NestedPublisher.Output,
|
||||
Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
private let downstream: Downstream
|
||||
private var outerSubscription: Subscription?
|
||||
private var currentInnerSubscription: Subscription?
|
||||
private var currentInnerIndex: UInt64 = 0
|
||||
private var nextInnerIndex: UInt64 = 1
|
||||
private let lock = UnfairLock.allocate()
|
||||
private let downstreamLock = UnfairRecursiveLock.allocate()
|
||||
private var cancelled = false
|
||||
private var finished = false
|
||||
private var sentCompletion = false
|
||||
private var awaitingInnerSubscription = false
|
||||
private var downstreamDemand = Subscribers.Demand.none
|
||||
|
||||
init(downstream: Downstream) {
|
||||
self.downstream = downstream
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
downstreamLock.deallocate()
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard outerSubscription == nil && !cancelled else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
outerSubscription = subscription
|
||||
lock.unlock()
|
||||
subscription.request(.unlimited)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
if cancelled || finished {
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
|
||||
if let currentInnerSubscription = self.currentInnerSubscription {
|
||||
self.currentInnerSubscription = nil
|
||||
lock.unlock()
|
||||
currentInnerSubscription.cancel()
|
||||
lock.lock()
|
||||
}
|
||||
|
||||
let index = nextInnerIndex
|
||||
currentInnerIndex = index
|
||||
nextInnerIndex += 1
|
||||
awaitingInnerSubscription = true
|
||||
lock.unlock()
|
||||
input.subscribe(Side(inner: self, index: index))
|
||||
return .none
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
lock.lock()
|
||||
outerSubscription = nil
|
||||
finished = true
|
||||
|
||||
if cancelled {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
|
||||
switch completion {
|
||||
case .finished:
|
||||
if awaitingInnerSubscription {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
if currentInnerSubscription == nil {
|
||||
sentCompletion = true
|
||||
lock.unlock()
|
||||
downstreamLock.lock()
|
||||
downstream.receive(completion: completion)
|
||||
downstreamLock.unlock()
|
||||
} else {
|
||||
lock.unlock()
|
||||
}
|
||||
case .failure:
|
||||
let currentInnerSubscription = self.currentInnerSubscription
|
||||
self.currentInnerSubscription = nil
|
||||
sentCompletion = true
|
||||
lock.unlock()
|
||||
currentInnerSubscription?.cancel()
|
||||
downstreamLock.lock()
|
||||
downstream.receive(completion: completion)
|
||||
downstreamLock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
demand.assertNonZero()
|
||||
lock.lock()
|
||||
downstreamDemand += demand
|
||||
if let currentInnerSubscription = self.currentInnerSubscription {
|
||||
lock.unlock()
|
||||
currentInnerSubscription.request(demand)
|
||||
} else {
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
cancelled = true
|
||||
let currentInnerSubscription = self.currentInnerSubscription
|
||||
self.currentInnerSubscription = nil
|
||||
let outerSubscription = self.outerSubscription
|
||||
self.outerSubscription = nil
|
||||
lock.unlock()
|
||||
|
||||
currentInnerSubscription?.cancel()
|
||||
outerSubscription?.cancel()
|
||||
}
|
||||
|
||||
var description: String { return "SwitchToLatest" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
return Mirror(self, children: EmptyCollection())
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
|
||||
private func receiveInner(subscription: Subscription, _ index: UInt64) {
|
||||
lock.lock()
|
||||
guard currentInnerIndex == index &&
|
||||
!cancelled &&
|
||||
currentInnerSubscription == nil else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
|
||||
currentInnerSubscription = subscription
|
||||
awaitingInnerSubscription = false
|
||||
let downstreamDemand = self.downstreamDemand
|
||||
lock.unlock()
|
||||
if downstreamDemand > 0 {
|
||||
subscription.request(downstreamDemand)
|
||||
}
|
||||
}
|
||||
|
||||
private func receiveInner(_ input: NestedPublisher.Output,
|
||||
_ index: UInt64) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
guard currentInnerIndex == index && !cancelled else {
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
|
||||
// This will crash if we don't have any demand yet.
|
||||
// Combine crashes here too.
|
||||
downstreamDemand -= 1
|
||||
|
||||
lock.unlock()
|
||||
downstreamLock.lock()
|
||||
let newDemand = downstream.receive(input)
|
||||
downstreamLock.unlock()
|
||||
if newDemand > 0 {
|
||||
lock.lock()
|
||||
downstreamDemand += newDemand
|
||||
lock.unlock()
|
||||
}
|
||||
|
||||
return newDemand
|
||||
}
|
||||
|
||||
private func receiveInner(completion: Subscribers.Completion<Failure>,
|
||||
_ index: UInt64) {
|
||||
lock.lock()
|
||||
guard currentInnerIndex == index && !cancelled else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
precondition(!awaitingInnerSubscription, "Unexpected completion")
|
||||
currentInnerSubscription = nil
|
||||
switch completion {
|
||||
case .finished:
|
||||
if sentCompletion || !finished {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
sentCompletion = true
|
||||
lock.unlock()
|
||||
downstreamLock.lock()
|
||||
downstream.receive(completion: completion)
|
||||
downstreamLock.unlock()
|
||||
case .failure:
|
||||
if sentCompletion {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
cancelled = true
|
||||
let outerSubscription = self.outerSubscription
|
||||
self.outerSubscription = nil
|
||||
sentCompletion = true
|
||||
lock.unlock()
|
||||
outerSubscription?.cancel()
|
||||
downstreamLock.lock()
|
||||
downstream.receive(completion: completion)
|
||||
downstreamLock.unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.SwitchToLatest.Outer {
|
||||
private struct Side
|
||||
: Subscriber,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
{
|
||||
typealias Input = NestedPublisher.Output
|
||||
|
||||
typealias Failure = NestedPublisher.Failure
|
||||
|
||||
typealias Outer =
|
||||
Publishers.SwitchToLatest<NestedPublisher, Upstream>.Outer<Downstream>
|
||||
|
||||
private let index: UInt64
|
||||
private let outer: Outer
|
||||
|
||||
let combineIdentifier = CombineIdentifier()
|
||||
|
||||
init(inner: Outer, index: UInt64) {
|
||||
self.index = index
|
||||
self.outer = inner
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
outer.receiveInner(subscription: subscription, index)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
return outer.receiveInner(input, index)
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
outer.receiveInner(completion: completion, index)
|
||||
}
|
||||
|
||||
var description: String { return "SwitchToLatest" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
let children = CollectionOfOne<Mirror.Child>(
|
||||
("parentSubscription", outer.combineIdentifier)
|
||||
)
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,6 @@
|
||||
// Created by Sergej Jaskiewicz on 12.11.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
/// A publisher that allows for recording a series of inputs and a completion for later
|
||||
/// playback to each subscriber.
|
||||
public struct Record<Output, Failure: Error>: Publisher {
|
||||
|
||||
@@ -61,6 +61,7 @@ extension Subscribers {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the result of adding two demands.
|
||||
/// When adding any value to `.unlimited`, the result is `.unlimited`.
|
||||
@inline(__always)
|
||||
@inlinable
|
||||
@@ -77,6 +78,7 @@ extension Subscribers {
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds two demands, and assigns the result to the first demand.
|
||||
/// When adding any value to `.unlimited`, the result is `.unlimited`.
|
||||
@inline(__always)
|
||||
@inlinable
|
||||
@@ -85,6 +87,7 @@ extension Subscribers {
|
||||
lhs = lhs + rhs
|
||||
}
|
||||
|
||||
/// Returns the result of adding an integer to a demand.
|
||||
/// When adding any value to` .unlimited`, the result is `.unlimited`.
|
||||
@inline(__always)
|
||||
@inlinable
|
||||
@@ -96,6 +99,7 @@ extension Subscribers {
|
||||
return isOverflow ? .unlimited : .max(sum)
|
||||
}
|
||||
|
||||
/// Adds an integer to a demand, and assigns the result to the demand.
|
||||
/// When adding any value to `.unlimited`, the result is `.unlimited`.
|
||||
@inline(__always)
|
||||
@inlinable
|
||||
@@ -103,6 +107,9 @@ extension Subscribers {
|
||||
lhs = lhs + rhs
|
||||
}
|
||||
|
||||
/// Returns the result of multiplying a demand by an integer.
|
||||
/// When multiplying any value by `.unlimited`, the result is `.unlimited`. If
|
||||
/// the multiplication operation overflows, the result is `.unlimited`.
|
||||
public static func * (lhs: Demand, rhs: Int) -> Demand {
|
||||
if lhs == .unlimited {
|
||||
return .unlimited
|
||||
@@ -112,12 +119,16 @@ extension Subscribers {
|
||||
return isOverflow ? .unlimited : .max(product)
|
||||
}
|
||||
|
||||
/// Multiplies a demand by an integer, and assigns the result to the demand.
|
||||
/// When multiplying any value by `.unlimited`, the result is `.unlimited`. If
|
||||
/// the multiplication operation overflows, the result is `.unlimited`.
|
||||
@inline(__always)
|
||||
@inlinable
|
||||
public static func *= (lhs: inout Demand, rhs: Int) {
|
||||
lhs = lhs * rhs
|
||||
}
|
||||
|
||||
/// Returns the result of subtracting one demand from another.
|
||||
/// When subtracting any value (including `.unlimited`) from `.unlimited`,
|
||||
/// the result is still `.unlimited`. Subtracting `.unlimited` from any value
|
||||
/// (except `.unlimited`) results in `.max(0)`. A negative demand is not possible;
|
||||
@@ -137,6 +148,7 @@ extension Subscribers {
|
||||
}
|
||||
}
|
||||
|
||||
/// Subtracts one demand from another, and assigns the result to the first demand.
|
||||
/// When subtracting any value (including `.unlimited`) from `.unlimited`,
|
||||
/// the result is still `.unlimited`. Subtracting unlimited from any value
|
||||
/// (except `.unlimited`) results in `.max(0)`. A negative demand is not possible;
|
||||
@@ -148,6 +160,7 @@ extension Subscribers {
|
||||
lhs = lhs - rhs
|
||||
}
|
||||
|
||||
/// Returns the result of subtracting an integer from a demand.
|
||||
/// When subtracting any value from `.unlimited`, the result is still
|
||||
/// `.unlimited`.
|
||||
/// A negative demand is not possible; any operation that would result in
|
||||
@@ -164,6 +177,7 @@ extension Subscribers {
|
||||
return isOverflow ? .none : .max(difference)
|
||||
}
|
||||
|
||||
/// Subtracts an integer from a demand, and assigns the result to the demand.
|
||||
/// When subtracting any value from `.unlimited,` the result is still
|
||||
/// `.unlimited`.
|
||||
/// A negative demand is not possible; any operation that would result in
|
||||
@@ -175,6 +189,10 @@ extension Subscribers {
|
||||
lhs = lhs - rhs
|
||||
}
|
||||
|
||||
/// Returns a Boolean that indicates whether the demand requests more than
|
||||
/// the given number of elements.
|
||||
/// If `lhs` is `.unlimited`, then the result is always `true`.
|
||||
/// Otherwise, the operator compares the demand’s `max` value to `rhs`.
|
||||
@inline(__always)
|
||||
@inlinable
|
||||
public static func > (lhs: Demand, rhs: Int) -> Bool {
|
||||
@@ -185,6 +203,10 @@ extension Subscribers {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a Boolean that indicates whether the first demand requests more or
|
||||
/// the same number of elements as the second.
|
||||
/// If `lhs` is `.unlimited`, then the result is always `true`.
|
||||
/// Otherwise, the operator compares the demand’s `max` value to `rhs`.
|
||||
@inline(__always)
|
||||
@inlinable
|
||||
public static func >= (lhs: Demand, rhs: Int) -> Bool {
|
||||
@@ -195,6 +217,10 @@ extension Subscribers {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a Boolean that indicates a given number of elements is greater than
|
||||
/// the maximum specified by the demand.
|
||||
/// If `rhs` is `.unlimited`, then the result is always `false`.
|
||||
/// Otherwise, the operator compares the demand’s `max` value to `lhs`.
|
||||
@inline(__always)
|
||||
@inlinable
|
||||
public static func > (lhs: Int, rhs: Demand) -> Bool {
|
||||
@@ -205,6 +231,10 @@ extension Subscribers {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a Boolean that indicates a given number of elements is greater than
|
||||
/// or equal to the maximum specified by the demand.
|
||||
/// If `rhs` is `.unlimited`, then the result is always `false`.
|
||||
/// Otherwise, the operator compares the demand’s `max` value to `lhs`.
|
||||
@inline(__always)
|
||||
@inlinable
|
||||
public static func >= (lhs: Int, rhs: Demand) -> Bool {
|
||||
@@ -215,6 +245,10 @@ extension Subscribers {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a Boolean that indicates whether the demand requests fewer than
|
||||
/// the given number of elements.
|
||||
/// If `lhs` is `.unlimited`, then the result is always `false`.
|
||||
/// Otherwise, the operator compares the demand’s `max` value to `rhs`.
|
||||
@inline(__always)
|
||||
@inlinable
|
||||
public static func < (lhs: Demand, rhs: Int) -> Bool {
|
||||
@@ -225,6 +259,10 @@ extension Subscribers {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a Boolean that indicates a given number of elements is less than
|
||||
/// the maximum specified by the demand.
|
||||
/// If `rhs` is `.unlimited`, then the result is always `true`.
|
||||
/// Otherwise, the operator compares the demand’s `max` value to `lhs`.
|
||||
@inline(__always)
|
||||
@inlinable
|
||||
public static func < (lhs: Int, rhs: Demand) -> Bool {
|
||||
@@ -235,6 +273,10 @@ extension Subscribers {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a Boolean that indicates whether the demand requests fewer or
|
||||
/// the same number of elements as the given integer.
|
||||
/// If `lhs` is `.unlimited`, then the result is always `false`.
|
||||
/// Otherwise, the operator compares the demand’s `max` value to `rhs`.
|
||||
@inline(__always)
|
||||
@inlinable
|
||||
public static func <= (lhs: Demand, rhs: Int) -> Bool {
|
||||
@@ -245,6 +287,10 @@ extension Subscribers {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a Boolean value that indicates a given number of elements is less
|
||||
/// than or equal the maximum specified by the demand.
|
||||
/// If `rhs` is `.unlimited`, then the result is always `true`.
|
||||
/// Otherwise, the operator compares the demand’s `max` value to `lhs`.
|
||||
@inline(__always)
|
||||
@inlinable
|
||||
public static func <= (lhs: Int, rhs: Demand) -> Bool {
|
||||
@@ -255,9 +301,12 @@ extension Subscribers {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a Boolean value that indicates whether the first demand requests fewer
|
||||
/// elements than the second.
|
||||
/// If both sides are `.unlimited`, the result is always `false`.
|
||||
/// If `lhs` is `.unlimited`, then the result is always `false`.
|
||||
/// If `rhs` is `.unlimited` then the result is `false` iff `lhs` is `.unlimited`
|
||||
/// Otherwise, the two `.max` values are compared.
|
||||
/// If `rhs` is `.unlimited`, then the result is always `true`.
|
||||
/// Otherwise, this operator compares the demands’ `max` values.
|
||||
@inline(__always)
|
||||
@inlinable
|
||||
public static func < (lhs: Demand, rhs: Demand) -> Bool {
|
||||
@@ -271,6 +320,12 @@ extension Subscribers {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a Boolean value that indicates whether the first demand requests fewer
|
||||
/// or the same number of elements as the second.
|
||||
/// If both sides are `.unlimited`, the result is always `true`.
|
||||
/// If `lhs` is `.unlimited`, then the result is always `false`.
|
||||
/// If `rhs` is `.unlimited` then the result is always `true`.
|
||||
/// Otherwise, this operator compares the demands’ `max` values.
|
||||
@inline(__always)
|
||||
@inlinable
|
||||
public static func <= (lhs: Demand, rhs: Demand) -> Bool {
|
||||
@@ -286,6 +341,12 @@ extension Subscribers {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a Boolean that indicates whether the first demand requests more or
|
||||
/// the same number of elements as the second.
|
||||
/// If both sides are `.unlimited`, the result is always `false`.
|
||||
/// If `lhs` is `.unlimited`, then the result is always `true`.
|
||||
/// If rhs is `.unlimited` then the result is always `false`.
|
||||
/// Otherwise, this operator compares the demands’ `max` values.
|
||||
@inline(__always)
|
||||
@inlinable
|
||||
public static func >= (lhs: Demand, rhs: Demand) -> Bool {
|
||||
@@ -301,6 +362,12 @@ extension Subscribers {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a Boolean that indicates whether the first demand requests more
|
||||
/// elements than the second.
|
||||
/// If both sides are `.unlimited`, the result is always `false`.
|
||||
/// If `lhs` is `.unlimited`, then the result is always `true`.
|
||||
/// If `rhs` is `.unlimited` then the result is always `false`.
|
||||
/// Otherwise, this operator compares the demands’ `max` values.
|
||||
@inline(__always)
|
||||
@inlinable
|
||||
public static func > (lhs: Demand, rhs: Demand) -> Bool {
|
||||
@@ -316,8 +383,9 @@ extension Subscribers {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if `lhs` and `rhs` are equal. `.unlimited` is not equal to any
|
||||
/// integer.
|
||||
/// Returns a Boolean value indicating whether a demand requests the given number
|
||||
/// of elements.
|
||||
/// An `.unlimited` demand doesn’t match any integer.
|
||||
@inline(__always)
|
||||
@inlinable
|
||||
public static func == (lhs: Demand, rhs: Int) -> Bool {
|
||||
@@ -328,8 +396,10 @@ extension Subscribers {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if `lhs` and `rhs` are not equal. `.unlimited` is not equal to
|
||||
/// any integer.
|
||||
/// Returns a Boolean value indicating whether a demand is not equal to
|
||||
/// an integer.
|
||||
/// The `.unlimited` value isn’t equal to any integer.
|
||||
@inlinable
|
||||
public static func != (lhs: Demand, rhs: Int) -> Bool {
|
||||
if lhs == .unlimited {
|
||||
return true
|
||||
@@ -338,8 +408,10 @@ extension Subscribers {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if `lhs` and `rhs` are equal. `.unlimited` is not equal to any
|
||||
/// integer.
|
||||
/// Returns a Boolean value indicating whether a given number of elements matches
|
||||
/// the request of a given demand.
|
||||
/// An `.unlimited` demand doesn’t match any integer.
|
||||
@inlinable
|
||||
public static func == (lhs: Int, rhs: Demand) -> Bool {
|
||||
if rhs == .unlimited {
|
||||
return false
|
||||
@@ -348,8 +420,10 @@ extension Subscribers {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if `lhs` and `rhs` are not equal. `.unlimited` is not equal to
|
||||
/// any integer.
|
||||
/// Returns a Boolean value indicating whether an integer is not equal to
|
||||
/// a demand.
|
||||
/// The `.unlimited` value isn’t equal to any integer.
|
||||
@inlinable
|
||||
public static func != (lhs: Int, rhs: Demand) -> Bool {
|
||||
if rhs == .unlimited {
|
||||
return true
|
||||
@@ -358,8 +432,13 @@ extension Subscribers {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the number of requested values, or `nil` if `.unlimited`.
|
||||
public var max: Int? {
|
||||
@inlinable
|
||||
public static func == (lhs: Demand, rhs: Demand) -> Bool {
|
||||
return lhs.rawValue == rhs.rawValue
|
||||
}
|
||||
|
||||
/// The number of requested values, or nil if `.unlimited`.
|
||||
@inlinable public var max: Int? {
|
||||
if self == .unlimited {
|
||||
return nil
|
||||
} else {
|
||||
|
||||
@@ -50,8 +50,12 @@ extension DispatchQueue {
|
||||
/// - Parameter other: Another dispatch queue time.
|
||||
/// - Returns: The time interval between this time and the provided time.
|
||||
public func distance(to other: SchedulerTimeType) -> Stride {
|
||||
let start = dispatchTime.rawValue
|
||||
let end = other.dispatchTime.rawValue
|
||||
return .nanoseconds(
|
||||
dispatchTime.rawValue.distance(to: other.dispatchTime.rawValue)
|
||||
end >= start
|
||||
? Int(Int64(bitPattern: end) - Int64(bitPattern: start))
|
||||
: -Int(Int64(bitPattern: start) - Int64(bitPattern: end))
|
||||
)
|
||||
}
|
||||
|
||||
@@ -62,7 +66,9 @@ extension DispatchQueue {
|
||||
/// - Returns: A dispatch queue time advanced by the given
|
||||
/// interval from this instance’s time.
|
||||
public func advanced(by stride: Stride) -> SchedulerTimeType {
|
||||
return .init(dispatchTime + stride.timeInterval)
|
||||
return stride.magnitude == .max
|
||||
? .init(.distantFuture)
|
||||
: .init(dispatchTime + stride.timeInterval)
|
||||
}
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
@@ -125,13 +131,52 @@ extension DispatchQueue {
|
||||
self = .microseconds(microseconds)
|
||||
case .nanoseconds(let nanoseconds):
|
||||
self = .nanoseconds(nanoseconds)
|
||||
// This dance is to avoid the warning 'default will never be executed'
|
||||
// on non-Darwin platforms.
|
||||
// There really shouldn't be a warning.
|
||||
// See https://forums.swift.org/t/unknown-default-produces-a-warning-on-linux-with-non-frozen-enum/31687
|
||||
//
|
||||
// Thanks to Jeremy David Giesbrecht for suggesting this workaround.
|
||||
#if canImport(Darwin)
|
||||
case .never:
|
||||
fallthrough
|
||||
@unknown default:
|
||||
self = .nanoseconds(.max)
|
||||
@unknown default:
|
||||
self.init(__guessFromUnknown: timeInterval)
|
||||
#else
|
||||
default:
|
||||
if case .never = timeInterval {
|
||||
self = .nanoseconds(.max)
|
||||
} else {
|
||||
self.init(__guessFromUnknown: timeInterval)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
public // testable
|
||||
init(__guessFromUnknown timeInterval: DispatchTimeInterval) {
|
||||
// Let's take some reference time,
|
||||
// add `timeInterval` to it, take the `rawValue` from the result
|
||||
// and subtract the `rawValue` of the reference time.
|
||||
//
|
||||
// We won't be able to provide the exact implementation though,
|
||||
// because something will definitely overflow.
|
||||
//
|
||||
// However, we can try to support as wide a range of values
|
||||
// as possible.
|
||||
//
|
||||
// By trial and error I got that the `rawValue` of `UInt64.max / 13`
|
||||
// gives us propably the widest range of supported values:
|
||||
// from `Int.min / 6.5` to `Int.max / 2.889` nanoseconds.
|
||||
// That's with Int being 64 bits. Since here only UInt64 can overflow,
|
||||
// when Int is 32 bits, we don't have this issue.
|
||||
// It should be more than enough.
|
||||
|
||||
let referenceTime = DispatchTime(uptimeNanoseconds: .max / 13)
|
||||
self = SchedulerTimeType(referenceTime)
|
||||
.distance(to: SchedulerTimeType(referenceTime + timeInterval))
|
||||
}
|
||||
|
||||
/// Creates a dispatch queue time interval from a floating-point
|
||||
/// seconds value.
|
||||
///
|
||||
@@ -191,15 +236,15 @@ extension DispatchQueue {
|
||||
}
|
||||
|
||||
public static func seconds(_ value: Int) -> Stride {
|
||||
return Stride(magnitude: value * 1_000_000_000)
|
||||
return Stride(magnitude: clampedIntProduct(value, 1_000_000_000))
|
||||
}
|
||||
|
||||
public static func milliseconds(_ value: Int) -> Stride {
|
||||
return Stride(magnitude: value * 1_000_000)
|
||||
return Stride(magnitude: clampedIntProduct(value, 1_000_000))
|
||||
}
|
||||
|
||||
public static func microseconds(_ value: Int) -> Stride {
|
||||
return Stride(magnitude: value * 1_000)
|
||||
return Stride(magnitude: clampedIntProduct(value, 1_000))
|
||||
}
|
||||
|
||||
public static func nanoseconds(_ value: Int) -> Stride {
|
||||
@@ -299,8 +344,10 @@ extension DispatchQueue {
|
||||
#if !canImport(Combine)
|
||||
extension DispatchQueue: OpenCombine.Scheduler {
|
||||
|
||||
/// Options that affect the operation of the dispatch queue scheduler.
|
||||
public typealias SchedulerOptions = OCombine.SchedulerOptions
|
||||
|
||||
/// The scheduler time type used by the dispatch queue.
|
||||
public typealias SchedulerTimeType = OCombine.SchedulerTimeType
|
||||
|
||||
public var minimumTolerance: OCombine.SchedulerTimeType.Stride {
|
||||
@@ -336,3 +383,18 @@ extension DispatchQueue: OpenCombine.Scheduler {
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// This function is taken from swift-corlibs-libdispatch:
|
||||
// https://github.com/apple/swift-corelibs-libdispatch/blob/c992dacf3ca114806e6ac9ffc9113b19255be9fe/src/swift/Time.swift#L134-L144
|
||||
//
|
||||
// Returns m1 * m2, clamped to the range [Int.min, Int.max].
|
||||
// Because of the way this function is used, we can always assume
|
||||
// that m2 > 0.
|
||||
private func clampedIntProduct(_ lhs: Int, _ rhs: Int) -> Int {
|
||||
assert(rhs > 0, "multiplier must be positive")
|
||||
let (result, overflow) = lhs.multipliedReportingOverflow(by: rhs)
|
||||
if overflow {
|
||||
return lhs > 0 ? .max : .min
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
//
|
||||
// Locking.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 10.12.2019.
|
||||
//
|
||||
|
||||
#if canImport(COpenCombineHelpers)
|
||||
import COpenCombineHelpers
|
||||
#endif
|
||||
|
||||
import OpenCombine
|
||||
|
||||
internal typealias UnfairLock = __UnfairLock
|
||||
internal typealias UnfairRecursiveLock = __UnfairRecursiveLock
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// Violations.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 13.12.2019.
|
||||
//
|
||||
|
||||
import OpenCombine
|
||||
|
||||
extension Subscribers.Demand {
|
||||
internal func assertNonZero(file: StaticString = #file,
|
||||
line: UInt = #line) {
|
||||
if self == .none {
|
||||
fatalError("API Violation: demand must not be zero", file: file, line: line)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// JSONEncoder.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 10.10.2019.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import OpenCombine
|
||||
|
||||
extension JSONEncoder: TopLevelEncoder {
|
||||
public typealias Output = Data
|
||||
}
|
||||
|
||||
extension JSONDecoder: TopLevelDecoder {
|
||||
public typealias Input = Data
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
//
|
||||
// NotificationCenter.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 10.10.2019.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import OpenCombine
|
||||
|
||||
extension NotificationCenter {
|
||||
|
||||
/// A namespace for disambiguation when both OpenCombine and Foundation are imported.
|
||||
///
|
||||
/// Foundation extends `NotificationCenter` with new methods and nested types.
|
||||
/// If you import both OpenCombine and Foundation, you will not be able
|
||||
/// to write `NotificationCenter.Publisher`,
|
||||
/// because Swift is unable to understand which `Publisher`
|
||||
/// you're referring to — the one declared in Foundation or in OpenCombine.
|
||||
///
|
||||
/// So you have to write `NotificationCenter.OCombine.Publisher`.
|
||||
///
|
||||
/// This bug is tracked [here](https://bugs.swift.org/browse/SR-11183).
|
||||
///
|
||||
/// You can omit this whenever Combine is not available (e. g. on Linux).
|
||||
public struct OCombine {
|
||||
|
||||
public let center: NotificationCenter
|
||||
|
||||
public init(_ center: NotificationCenter) {
|
||||
self.center = center
|
||||
}
|
||||
|
||||
/// A publisher that emits elements when broadcasting notifications.
|
||||
public struct Publisher: OpenCombine.Publisher {
|
||||
|
||||
public typealias Output = Notification
|
||||
|
||||
public typealias Failure = Never
|
||||
|
||||
/// The notification center this publisher uses as a source.
|
||||
public let center: NotificationCenter
|
||||
|
||||
/// The name of notifications published by this publisher.
|
||||
public let name: Notification.Name
|
||||
|
||||
/// The object posting the named notfication.
|
||||
public let object: AnyObject?
|
||||
|
||||
/// Creates a publisher that emits events when broadcasting notifications.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - center: The notification center to publish notifications for.
|
||||
/// - name: The name of the notification to publish.
|
||||
/// - object: The object posting the named notfication. If `nil`,
|
||||
/// the publisher emits elements for any object producing a notification
|
||||
/// with the given name.
|
||||
public init(center: NotificationCenter,
|
||||
name: Notification.Name,
|
||||
object: AnyObject? = nil) {
|
||||
self.center = center
|
||||
self.name = name
|
||||
self.object = object
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Failure == Never, Downstream.Input == Notification
|
||||
{
|
||||
let subscription = Notification.Subscription(center: center,
|
||||
name: name,
|
||||
object: object,
|
||||
downstream: subscriber)
|
||||
subscriber.receive(subscription: subscription)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a publisher that emits events when broadcasting notifications.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - name: The name of the notification to publish.
|
||||
/// - object: The object posting the named notfication. If `nil`, the publisher
|
||||
/// emits elements for any object producing a notification with the given
|
||||
/// name.
|
||||
/// - Returns: A publisher that emits events when broadcasting notifications.
|
||||
public func publisher(for name: Notification.Name,
|
||||
object: AnyObject? = nil) -> Publisher {
|
||||
return .init(center: center, name: name, object: object)
|
||||
}
|
||||
}
|
||||
|
||||
#if !canImport(Combine)
|
||||
/// A publisher that emits elements when broadcasting notifications.
|
||||
public typealias Publisher = OCombine.Publisher
|
||||
#endif
|
||||
}
|
||||
|
||||
extension NotificationCenter {
|
||||
|
||||
/// A namespace for disambiguation when both OpenCombine and Foundation are imported.
|
||||
///
|
||||
/// Foundation extends `NotificationCenter` with new methods and nested types.
|
||||
/// If you import both OpenCombine and Foundation, you will not be able
|
||||
/// to write `NotificationCenter.default.publisher(for: name)`,
|
||||
/// because Swift is unable to understand which `publisher` method
|
||||
/// you're referring to — the one declared in Foundation or in OpenCombine.
|
||||
///
|
||||
/// So you have to write `NotificationCenter.default.ocombine.publisher(for: name)`.
|
||||
///
|
||||
/// This bug is tracked [here](https://bugs.swift.org/browse/SR-11183).
|
||||
///
|
||||
/// You can omit this whenever Combine is not available (e. g. on Linux).
|
||||
public var ocombine: OCombine { return .init(self) }
|
||||
|
||||
#if !canImport(Combine)
|
||||
/// Returns a publisher that emits events when broadcasting notifications.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - name: The name of the notification to publish.
|
||||
/// - object: The object posting the named notfication. If `nil`, the publisher
|
||||
/// emits elements for any object producing a notification with the given name.
|
||||
/// - Returns: A publisher that emits events when broadcasting notifications.
|
||||
public func publisher(for name: Notification.Name,
|
||||
object: AnyObject? = nil) -> OCombine.Publisher {
|
||||
return ocombine.publisher(for: name, object: object)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
extension NotificationCenter.OCombine.Publisher: Equatable {
|
||||
public static func == (lhs: NotificationCenter.OCombine.Publisher,
|
||||
rhs: NotificationCenter.OCombine.Publisher) -> Bool {
|
||||
return lhs.center == rhs.center &&
|
||||
lhs.name == rhs.name &&
|
||||
lhs.object === rhs.object
|
||||
}
|
||||
}
|
||||
|
||||
extension Notification {
|
||||
fileprivate final class Subscription<Downstream: Subscriber>
|
||||
: OpenCombine.Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Notification, Downstream.Failure == Never
|
||||
{
|
||||
private let lock = UnfairLock.allocate()
|
||||
|
||||
fileprivate let downstreamLock = UnfairRecursiveLock.allocate()
|
||||
|
||||
fileprivate var demand = Subscribers.Demand.none
|
||||
|
||||
private var center: NotificationCenter?
|
||||
|
||||
private let name: Name
|
||||
|
||||
private var object: AnyObject?
|
||||
|
||||
private var observation: AnyObject?
|
||||
|
||||
fileprivate init(center: NotificationCenter,
|
||||
name: Notification.Name,
|
||||
object: AnyObject?,
|
||||
downstream: Downstream) {
|
||||
self.center = center
|
||||
self.name = name
|
||||
self.object = object
|
||||
self.observation = center
|
||||
.addObserver(forName: name, object: object, queue: nil) { [weak self] in
|
||||
self?.didReceiveNotification($0, downstream: downstream)
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
downstreamLock.deallocate()
|
||||
}
|
||||
|
||||
private func didReceiveNotification(_ notification: Notification,
|
||||
downstream: Downstream) {
|
||||
lock.lock()
|
||||
guard demand > 0 else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
demand -= 1
|
||||
lock.unlock()
|
||||
downstreamLock.lock()
|
||||
let newDemand = downstream.receive(notification)
|
||||
downstreamLock.unlock()
|
||||
lock.lock()
|
||||
demand += newDemand
|
||||
lock.unlock()
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
lock.lock()
|
||||
self.demand += demand
|
||||
lock.unlock()
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
guard let center = self.center, let observation = self.observation else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
self.center = nil
|
||||
self.object = nil
|
||||
self.observation = nil
|
||||
lock.unlock()
|
||||
center.removeObserver(observation)
|
||||
}
|
||||
|
||||
fileprivate var description: String { return "NotificationCenter Observer" }
|
||||
|
||||
fileprivate var customMirror: Mirror {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
let children: [Mirror.Child] = [
|
||||
("center", center as Any),
|
||||
("name", name),
|
||||
("object", object as Any),
|
||||
("demand", demand)
|
||||
]
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
fileprivate var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// PropertyListEncoder.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 10.12.2019.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import OpenCombine
|
||||
|
||||
extension PropertyListEncoder: TopLevelEncoder {
|
||||
public typealias Output = Data
|
||||
}
|
||||
|
||||
extension PropertyListDecoder: TopLevelDecoder {
|
||||
public typealias Input = Data
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
//
|
||||
// RunLoop.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 13.12.2019.
|
||||
//
|
||||
|
||||
import CoreFoundation
|
||||
import Dispatch
|
||||
import Foundation
|
||||
import OpenCombine
|
||||
|
||||
extension RunLoop {
|
||||
|
||||
/// A namespace for disambiguation when both OpenCombine and Combine are imported.
|
||||
///
|
||||
/// Foundation overlay for Combine extends `RunLoop` with new methods and nested
|
||||
/// types.
|
||||
/// If you import both OpenCombine and Foundation, you will not be able
|
||||
/// to write `RunLoop.SchedulerTimeType`,
|
||||
/// because Swift is unable to understand which `SchedulerTimeType`
|
||||
/// you're referring to.
|
||||
///
|
||||
/// So you have to write `RunLoop.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 runLoop: RunLoop
|
||||
|
||||
public init(_ queue: RunLoop) {
|
||||
self.runLoop = queue
|
||||
}
|
||||
|
||||
/// The scheduler time type used by the run loop.
|
||||
public struct SchedulerTimeType: Strideable, Codable, Hashable {
|
||||
|
||||
/// The date represented by this type.
|
||||
public var date: Date
|
||||
|
||||
/// Initializes a run loop scheduler time with the given date.
|
||||
///
|
||||
/// - Parameter date: The date to represent.
|
||||
public init(_ date: Date) {
|
||||
self.date = date
|
||||
}
|
||||
|
||||
/// Returns the distance to another run loop scheduler time.
|
||||
///
|
||||
/// - Parameter other: Another run loop 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 a run loop scheduler time calculated by advancing this instance’s
|
||||
/// time by the given interval.
|
||||
///
|
||||
/// - Parameter value: A time interval to advance.
|
||||
/// - Returns: A run loop time advanced by the given interval from this
|
||||
/// instance’s time.
|
||||
public func advanced(by value: Stride) -> SchedulerTimeType {
|
||||
return SchedulerTimeType(date + value.magnitude)
|
||||
}
|
||||
|
||||
/// The interval by which run loop times advance.
|
||||
public struct Stride: SchedulerTimeIntervalConvertible,
|
||||
Comparable,
|
||||
SignedNumeric,
|
||||
ExpressibleByFloatLiteral,
|
||||
Codable {
|
||||
|
||||
public typealias FloatLiteralType = TimeInterval
|
||||
|
||||
public typealias IntegerLiteralType = TimeInterval
|
||||
|
||||
/// A type that can represent the absolute value of any possible value
|
||||
/// of the conforming type.
|
||||
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) {
|
||||
self.magnitude = value
|
||||
}
|
||||
|
||||
public init(floatLiteral value: TimeInterval) {
|
||||
self.magnitude = value
|
||||
}
|
||||
|
||||
public init(_ timeInterval: TimeInterval) {
|
||||
self.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 run loop scheduler.
|
||||
public struct SchedulerOptions {
|
||||
}
|
||||
|
||||
public func schedule(options: SchedulerOptions?, _ action: @escaping () -> Void) {
|
||||
let cfRunLoop = runLoop.getCFRunLoop()
|
||||
CFRunLoopPerformBlock(cfRunLoop, defaultRunLoopModeString, action)
|
||||
CFRunLoopWakeUp(cfRunLoop)
|
||||
}
|
||||
|
||||
public func schedule(after date: SchedulerTimeType,
|
||||
tolerance: SchedulerTimeType.Stride,
|
||||
options: SchedulerOptions?,
|
||||
_ action: @escaping () -> Void) {
|
||||
let timer = CFRunLoopTimerCreateWithHandler(
|
||||
nil,
|
||||
date.date.timeIntervalSinceReferenceDate,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
{ _ in action() }
|
||||
)
|
||||
// A bug in Combine. The schedule(after:tolerance:options:_:) methods
|
||||
// always executes the action on the current runloop.
|
||||
// (FB7493579 if Apple folks are watching)
|
||||
let theWrongRunLoop = CFRunLoopGetCurrent()
|
||||
CFRunLoopAddTimer(theWrongRunLoop, timer, defaultRunLoopMode)
|
||||
CFRunLoopWakeUp(theWrongRunLoop)
|
||||
}
|
||||
|
||||
public func schedule(after date: SchedulerTimeType,
|
||||
interval: SchedulerTimeType.Stride,
|
||||
tolerance: SchedulerTimeType.Stride,
|
||||
options: SchedulerOptions?,
|
||||
_ action: @escaping () -> Void) -> Cancellable {
|
||||
let timer = CFRunLoopTimerCreateWithHandler(
|
||||
nil,
|
||||
date.date.timeIntervalSinceReferenceDate,
|
||||
interval.magnitude,
|
||||
0,
|
||||
0,
|
||||
{ _ in action() }
|
||||
)
|
||||
let cfRunLoop = runLoop.getCFRunLoop()
|
||||
CFRunLoopAddTimer(cfRunLoop, timer, defaultRunLoopMode)
|
||||
CFRunLoopWakeUp(cfRunLoop)
|
||||
return AnyCancellable { CFRunLoopTimerInvalidate(timer) }
|
||||
}
|
||||
|
||||
public var now: SchedulerTimeType {
|
||||
return .init(Date())
|
||||
}
|
||||
|
||||
public var minimumTolerance: SchedulerTimeType.Stride {
|
||||
return .init(0)
|
||||
}
|
||||
}
|
||||
|
||||
/// A namespace for disambiguation when both OpenCombine and Foundation are imported.
|
||||
///
|
||||
/// Foundation overlay for Combine extends `RunLoop` with new methods and nested
|
||||
/// types.
|
||||
/// If you import both OpenCombine and Foundation, you will not be able
|
||||
/// to write `RunLoop.main.schedule { doThings() }`,
|
||||
/// because Swift is unable to understand which `schedule` method
|
||||
/// you're referring to.
|
||||
///
|
||||
/// So you have to write `RunLoop.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 RunLoop: 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
|
||||
|
||||
private var defaultRunLoopMode: CFRunLoopMode {
|
||||
#if canImport(Darwin)
|
||||
return CFRunLoopMode.defaultMode
|
||||
#else
|
||||
return kCFRunLoopDefaultMode
|
||||
#endif
|
||||
}
|
||||
|
||||
private var defaultRunLoopModeString: CFString {
|
||||
#if canImport(Darwin)
|
||||
return CFRunLoopMode.defaultMode.rawValue
|
||||
#else
|
||||
return kCFRunLoopDefaultMode
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
//
|
||||
// URLSession.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 13.12.2019.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
#if canImport(FoundationNetworking)
|
||||
import FoundationNetworking
|
||||
#endif
|
||||
|
||||
import OpenCombine
|
||||
|
||||
extension URLSession {
|
||||
|
||||
/// A namespace for disambiguation when both OpenCombine and Foundation are imported.
|
||||
///
|
||||
/// Foundation extends `URLSession` with new methods and nested types.
|
||||
/// If you import both OpenCombine and Foundation, you will not be able
|
||||
/// to write `URLSession.DataTaskPublisher`,
|
||||
/// because Swift is unable to understand which `DataTaskPublisher`
|
||||
/// you're referring to — the one declared in Foundation or in OpenCombine.
|
||||
///
|
||||
/// So you have to write `URLSession.OCombine.DataTaskPublisher`.
|
||||
///
|
||||
/// This bug is tracked [here](https://bugs.swift.org/browse/SR-11183).
|
||||
///
|
||||
/// You can omit this whenever Combine is not available (e. g. on Linux).
|
||||
public struct OCombine {
|
||||
|
||||
public let session: URLSession
|
||||
|
||||
public init(_ session: URLSession) {
|
||||
self.session = session
|
||||
}
|
||||
|
||||
public struct DataTaskPublisher: Publisher {
|
||||
|
||||
public typealias Output = (data: Data, response: URLResponse)
|
||||
|
||||
public typealias Failure = URLError
|
||||
|
||||
public let request: URLRequest
|
||||
|
||||
public let session: URLSession
|
||||
|
||||
public init(request: URLRequest, session: URLSession) {
|
||||
self.request = request
|
||||
self.session = session
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Failure == Failure, Downstream.Input == Output
|
||||
{
|
||||
let subscription = Inner(parent: self, downstream: subscriber)
|
||||
subscriber.receive(subscription: subscription)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a publisher that wraps a URL session data task for a given URL.
|
||||
///
|
||||
/// The publisher publishes data when the task completes, or terminates if
|
||||
/// the task fails with an error.
|
||||
///
|
||||
/// - Parameter url: The URL for which to create a data task.
|
||||
/// - Returns: A publisher that wraps a data task for the URL.
|
||||
public func dataTaskPublisher(for url: URL) -> DataTaskPublisher {
|
||||
return dataTaskPublisher(for: URLRequest(url: url))
|
||||
}
|
||||
|
||||
/// Returns a publisher that wraps a URL session data task for a given
|
||||
/// URL request.
|
||||
///
|
||||
/// The publisher publishes data when the task completes, or terminates if
|
||||
/// the task fails with an error.
|
||||
///
|
||||
/// - Parameter request: The URL request for which to create a data task.
|
||||
/// - Returns: A publisher that wraps a data task for the URL request.
|
||||
public func dataTaskPublisher(for request: URLRequest) -> DataTaskPublisher {
|
||||
return .init(request: request, session: session)
|
||||
}
|
||||
}
|
||||
|
||||
#if !canImport(Combine)
|
||||
public typealias DataTaskPublisher = OCombine.DataTaskPublisher
|
||||
#endif
|
||||
}
|
||||
|
||||
extension URLSession {
|
||||
|
||||
/// A namespace for disambiguation when both OpenCombine and Foundation are imported.
|
||||
///
|
||||
/// Foundation extends `URLSession` with new methods and nested types.
|
||||
/// If you import both OpenCombine and Foundation, you will not be able
|
||||
/// to write `URLSession.shared.dataTaskPublisher(for: url)`,
|
||||
/// because Swift is unable to understand which `dataTaskPublisher` method
|
||||
/// you're referring to — the one declared in Foundation or in OpenCombine.
|
||||
///
|
||||
/// So you have to write `URLSession.shared.ocombine.dataTaskPublisher(for: url)`.
|
||||
///
|
||||
/// This bug is tracked [here](https://bugs.swift.org/browse/SR-11183).
|
||||
///
|
||||
/// You can omit this whenever Combine is not available (e. g. on Linux).
|
||||
public var ocombine: OCombine { return .init(self) }
|
||||
|
||||
#if !canImport(Combine)
|
||||
/// Returns a publisher that wraps a URL session data task for a given URL.
|
||||
///
|
||||
/// The publisher publishes data when the task completes, or terminates if the task
|
||||
/// fails with an error.
|
||||
///
|
||||
/// - Parameter url: The URL for which to create a data task.
|
||||
/// - Returns: A publisher that wraps a data task for the URL.
|
||||
public func dataTaskPublisher(for url: URL) -> DataTaskPublisher {
|
||||
return ocombine.dataTaskPublisher(for: url)
|
||||
}
|
||||
|
||||
/// Returns a publisher that wraps a URL session data task for a given URL request.
|
||||
///
|
||||
/// The publisher publishes data when the task completes, or terminates if the task
|
||||
/// fails with an error.
|
||||
///
|
||||
/// - Parameter request: The URL request for which to create a data task.
|
||||
/// - Returns: A publisher that wraps a data task for the URL request.
|
||||
public func dataTaskPublisher(for request: URLRequest) -> DataTaskPublisher {
|
||||
return ocombine.dataTaskPublisher(for: request)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
extension URLSession.OCombine.DataTaskPublisher {
|
||||
private class Inner<Downstream: Subscriber>
|
||||
: Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == (data: Data, response: URLResponse),
|
||||
Downstream.Failure == URLError
|
||||
{
|
||||
private let lock = UnfairLock.allocate()
|
||||
|
||||
private var parent: URLSession.OCombine.DataTaskPublisher?
|
||||
|
||||
private var downstream: Downstream?
|
||||
|
||||
private var demand = Subscribers.Demand.none
|
||||
|
||||
private var task: URLSessionDataTask?
|
||||
|
||||
fileprivate init(parent: URLSession.OCombine.DataTaskPublisher,
|
||||
downstream: Downstream) {
|
||||
self.parent = parent
|
||||
self.downstream = downstream
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
demand.assertNonZero()
|
||||
lock.lock()
|
||||
guard let parent = self.parent else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
if self.task == nil {
|
||||
task = parent.session.dataTask(with: parent.request,
|
||||
completionHandler: handleResponse)
|
||||
}
|
||||
self.demand += demand
|
||||
let task = self.task
|
||||
lock.unlock()
|
||||
task?.resume()
|
||||
}
|
||||
|
||||
private func handleResponse(data: Data?, response: URLResponse?, error: Error?) {
|
||||
lock.lock()
|
||||
guard demand > 0, parent != nil, let downstream = self.downstream else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
lockedTerminate()
|
||||
lock.unlock()
|
||||
switch (data, response, error) {
|
||||
case let (data, response?, nil):
|
||||
_ = downstream.receive((data ?? Data(), response))
|
||||
downstream.receive(completion: .finished)
|
||||
case let (_, _, error as URLError):
|
||||
downstream.receive(completion: .failure(error))
|
||||
default:
|
||||
downstream.receive(completion: .failure(URLError(.unknown)))
|
||||
}
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
guard parent != nil else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
let task = self.task
|
||||
lockedTerminate()
|
||||
lock.unlock()
|
||||
task?.cancel()
|
||||
}
|
||||
|
||||
private func lockedTerminate() {
|
||||
parent = nil
|
||||
downstream = nil
|
||||
demand = .none
|
||||
task = nil
|
||||
}
|
||||
|
||||
var description: String { return "DataTaskPublisher" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
let children: [Mirror.Child] = [
|
||||
("task", task as Any),
|
||||
("downstream", downstream as Any),
|
||||
("parent", parent as Any),
|
||||
("demand", demand)
|
||||
]
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
@@ -23,13 +23,34 @@ final class DispatchQueueSchedulerTests: XCTestCase {
|
||||
func testSchedulerTimeTypeDistance() {
|
||||
let time1 = Scheduler.SchedulerTimeType(.init(uptimeNanoseconds: 10000))
|
||||
let time2 = Scheduler.SchedulerTimeType(.init(uptimeNanoseconds: 10431))
|
||||
let distantFuture = Scheduler.SchedulerTimeType(.distantFuture)
|
||||
let notSoDistantFuture = Scheduler.SchedulerTimeType(
|
||||
DispatchTime(
|
||||
uptimeNanoseconds: DispatchTime.distantFuture.uptimeNanoseconds - 1024
|
||||
)
|
||||
)
|
||||
|
||||
XCTAssertEqual(time1.distance(to: time2), .nanoseconds(431))
|
||||
XCTAssertEqual(time2.distance(to: time1), .nanoseconds(-431))
|
||||
|
||||
XCTAssertEqual(time1.distance(to: distantFuture), .nanoseconds(-10001))
|
||||
XCTAssertEqual(distantFuture.distance(to: time1), .nanoseconds(10001))
|
||||
XCTAssertEqual(time2.distance(to: distantFuture), .nanoseconds(-10432))
|
||||
XCTAssertEqual(distantFuture.distance(to: time2), .nanoseconds(10432))
|
||||
|
||||
XCTAssertEqual(time1.distance(to: notSoDistantFuture), .nanoseconds(-11025))
|
||||
XCTAssertEqual(notSoDistantFuture.distance(to: time1), .nanoseconds(11025))
|
||||
XCTAssertEqual(time2.distance(to: notSoDistantFuture), .nanoseconds(-11456))
|
||||
XCTAssertEqual(notSoDistantFuture.distance(to: time2), .nanoseconds(11456))
|
||||
|
||||
XCTAssertEqual(distantFuture.distance(to: distantFuture), .nanoseconds(0))
|
||||
XCTAssertEqual(notSoDistantFuture.distance(to: notSoDistantFuture),
|
||||
.nanoseconds(0))
|
||||
}
|
||||
|
||||
func testSchedulerTimeTypeAdvanced() {
|
||||
let time = Scheduler.SchedulerTimeType(.init(uptimeNanoseconds: 10000))
|
||||
let beginningOfTime = Scheduler.SchedulerTimeType(.init(uptimeNanoseconds: 1))
|
||||
let stride1 = Scheduler.SchedulerTimeType.Stride.nanoseconds(431)
|
||||
let stride2 = Scheduler.SchedulerTimeType.Stride.nanoseconds(-220)
|
||||
|
||||
@@ -38,6 +59,12 @@ final class DispatchQueueSchedulerTests: XCTestCase {
|
||||
|
||||
XCTAssertEqual(time.advanced(by: stride2),
|
||||
Scheduler.SchedulerTimeType(.init(uptimeNanoseconds: 9780)))
|
||||
|
||||
XCTAssertEqual(time.advanced(by: .nanoseconds(.max)).dispatchTime,
|
||||
.distantFuture)
|
||||
|
||||
XCTAssertEqual(beginningOfTime.advanced(by: .nanoseconds(-1000)).dispatchTime,
|
||||
DispatchTime(uptimeNanoseconds: 1))
|
||||
}
|
||||
|
||||
func testSchedulerTimeTypeEquatable() {
|
||||
@@ -84,41 +111,145 @@ final class DispatchQueueSchedulerTests: XCTestCase {
|
||||
// MARK: - Scheduler.SchedulerTimeType.Stride
|
||||
|
||||
func testStrideToDispatchTimeInterval() {
|
||||
typealias Stride = Scheduler.SchedulerTimeType.Stride
|
||||
|
||||
switch (Stride.seconds(2).timeInterval,
|
||||
Stride.milliseconds(2).timeInterval,
|
||||
Stride.microseconds(2).timeInterval,
|
||||
Stride.nanoseconds(2).timeInterval) {
|
||||
Stride.nanoseconds(2).timeInterval,
|
||||
Stride.nanoseconds(.max).timeInterval) {
|
||||
case (.nanoseconds(2_000_000_000),
|
||||
.nanoseconds(2_000_000),
|
||||
.nanoseconds(2_000),
|
||||
.nanoseconds(2)):
|
||||
.nanoseconds(2),
|
||||
.nanoseconds(.max)):
|
||||
break // pass
|
||||
case let intervals:
|
||||
XCTFail("Unexpected DispatchTimeInterval: \(intervals)")
|
||||
}
|
||||
}
|
||||
|
||||
func testStrideFromDispatchTimeInterval() {
|
||||
typealias Stride = Scheduler.SchedulerTimeType.Stride
|
||||
|
||||
func testStrideFromDispatchTimeInterval() throws {
|
||||
XCTAssertEqual(Stride(.seconds(2)).magnitude, 2_000_000_000)
|
||||
XCTAssertEqual(Stride(.milliseconds(2)).magnitude, 2_000_000)
|
||||
XCTAssertEqual(Stride(.microseconds(2)).magnitude, 2_000)
|
||||
XCTAssertEqual(Stride(.nanoseconds(2)).magnitude, 2)
|
||||
|
||||
XCTAssertEqual(Stride(.never).magnitude, .max)
|
||||
XCTAssertEqual(Stride(.nanoseconds(.max)).magnitude, .max)
|
||||
XCTAssertEqual(Stride(.nanoseconds(.min)).magnitude, .min)
|
||||
XCTAssertEqual(Stride(.microseconds(.max)).magnitude, .max)
|
||||
XCTAssertEqual(Stride(.microseconds(.min)).magnitude, .min)
|
||||
XCTAssertEqual(Stride(.milliseconds(.max)).magnitude, .max)
|
||||
XCTAssertEqual(Stride(.milliseconds(.min)).magnitude, .min)
|
||||
XCTAssertEqual(Stride(.seconds(.max)).magnitude, .max)
|
||||
XCTAssertEqual(Stride(.seconds(.min)).magnitude, .min)
|
||||
}
|
||||
|
||||
func testStrideFromUnknownDispatchTimeIntervalCase() {
|
||||
// Here we're testing out internal API that is not present in Combine.
|
||||
// Although we prefer only testing public APIs, this case is special.
|
||||
let makeStride: (DispatchTimeInterval) -> Stride
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
makeStride = Stride.init(_:)
|
||||
#else
|
||||
makeStride = Stride.init(__guessFromUnknown:)
|
||||
#endif
|
||||
|
||||
#if arch(x86_64) || arch(arm64) || arch(s390x) || arch(powerpc64) || arch(powerpc64le)
|
||||
// 64-bit platforms
|
||||
let minNanoseconds = -0x13B13B13B13B13B0 // Int64.min / 6.5
|
||||
let maxNanoseconds = 0x2C4EC4EC4EC4EC4D // Int64.max / 2.889
|
||||
#elseif arch(i386) || arch(arm)
|
||||
// 32-bit platforms
|
||||
let minNanoseconds = Int.min + 1
|
||||
let maxNanoseconds = Int.max
|
||||
#else
|
||||
#error("This architecture isn't known. Add it to the 32-bit or 64-bit line.")
|
||||
#endif
|
||||
XCTAssertEqual(makeStride(.nanoseconds(minNanoseconds)).magnitude, minNanoseconds)
|
||||
XCTAssertEqual(makeStride(.nanoseconds(-128)).magnitude, -128)
|
||||
XCTAssertEqual(makeStride(.nanoseconds(-57)).magnitude, -57)
|
||||
XCTAssertEqual(makeStride(.nanoseconds(-33)).magnitude, -33)
|
||||
XCTAssertEqual(makeStride(.nanoseconds(-17)).magnitude, -17)
|
||||
XCTAssertEqual(makeStride(.nanoseconds(-8)).magnitude, -8)
|
||||
XCTAssertEqual(makeStride(.nanoseconds(-3)).magnitude, -3)
|
||||
XCTAssertEqual(makeStride(.nanoseconds(-1)).magnitude, -1)
|
||||
XCTAssertEqual(makeStride(.nanoseconds(0)).magnitude, 0)
|
||||
XCTAssertEqual(makeStride(.nanoseconds(1)).magnitude, 1)
|
||||
XCTAssertEqual(makeStride(.nanoseconds(3)).magnitude, 3)
|
||||
XCTAssertEqual(makeStride(.nanoseconds(8)).magnitude, 8)
|
||||
XCTAssertEqual(makeStride(.nanoseconds(17)).magnitude, 17)
|
||||
XCTAssertEqual(makeStride(.nanoseconds(33)).magnitude, 33)
|
||||
XCTAssertEqual(makeStride(.nanoseconds(57)).magnitude, 57)
|
||||
XCTAssertEqual(makeStride(.nanoseconds(128)).magnitude, 128)
|
||||
XCTAssertEqual(makeStride(.nanoseconds(maxNanoseconds)).magnitude, maxNanoseconds)
|
||||
|
||||
XCTAssertEqual(makeStride(.microseconds(-128)).magnitude, -128_000)
|
||||
XCTAssertEqual(makeStride(.microseconds(-57)).magnitude, -57_000)
|
||||
XCTAssertEqual(makeStride(.microseconds(-33)).magnitude, -33_000)
|
||||
XCTAssertEqual(makeStride(.microseconds(-17)).magnitude, -17_000)
|
||||
XCTAssertEqual(makeStride(.microseconds(-8)).magnitude, -8_000)
|
||||
XCTAssertEqual(makeStride(.microseconds(-3)).magnitude, -3_000)
|
||||
XCTAssertEqual(makeStride(.microseconds(-1)).magnitude, -1_000)
|
||||
XCTAssertEqual(makeStride(.microseconds(0)).magnitude, 0)
|
||||
XCTAssertEqual(makeStride(.microseconds(1)).magnitude, 1_000)
|
||||
XCTAssertEqual(makeStride(.microseconds(3)).magnitude, 3_000)
|
||||
XCTAssertEqual(makeStride(.microseconds(8)).magnitude, 8_000)
|
||||
XCTAssertEqual(makeStride(.microseconds(17)).magnitude, 17_000)
|
||||
XCTAssertEqual(makeStride(.microseconds(33)).magnitude, 33_000)
|
||||
XCTAssertEqual(makeStride(.microseconds(57)).magnitude, 57_000)
|
||||
XCTAssertEqual(makeStride(.microseconds(128)).magnitude, 128_000)
|
||||
|
||||
XCTAssertEqual(makeStride(.milliseconds(-128)).magnitude, -128_000_000)
|
||||
XCTAssertEqual(makeStride(.milliseconds(-57)).magnitude, -57_000_000)
|
||||
XCTAssertEqual(makeStride(.milliseconds(-33)).magnitude, -33_000_000)
|
||||
XCTAssertEqual(makeStride(.milliseconds(-17)).magnitude, -17_000_000)
|
||||
XCTAssertEqual(makeStride(.milliseconds(-8)).magnitude, -8_000_000)
|
||||
XCTAssertEqual(makeStride(.milliseconds(-3)).magnitude, -3_000_000)
|
||||
XCTAssertEqual(makeStride(.milliseconds(-1)).magnitude, -1_000_000)
|
||||
XCTAssertEqual(makeStride(.milliseconds(0)).magnitude, 0)
|
||||
XCTAssertEqual(makeStride(.milliseconds(1)).magnitude, 1_000_000)
|
||||
XCTAssertEqual(makeStride(.milliseconds(3)).magnitude, 3_000_000)
|
||||
XCTAssertEqual(makeStride(.milliseconds(8)).magnitude, 8_000_000)
|
||||
XCTAssertEqual(makeStride(.milliseconds(17)).magnitude, 17_000_000)
|
||||
XCTAssertEqual(makeStride(.milliseconds(33)).magnitude, 33_000_000)
|
||||
XCTAssertEqual(makeStride(.milliseconds(57)).magnitude, 57_000_000)
|
||||
XCTAssertEqual(makeStride(.milliseconds(128)).magnitude, 128_000_000)
|
||||
|
||||
XCTAssertEqual(makeStride(.seconds(-2)).magnitude, -2_000_000_000)
|
||||
XCTAssertEqual(makeStride(.seconds(-1)).magnitude, -1_000_000_000)
|
||||
XCTAssertEqual(makeStride(.seconds(0)).magnitude, 0)
|
||||
XCTAssertEqual(makeStride(.seconds(1)).magnitude, 1_000_000_000)
|
||||
XCTAssertEqual(makeStride(.seconds(2)).magnitude, 2_000_000_000)
|
||||
}
|
||||
|
||||
func testStrideFromNumericValue() {
|
||||
typealias Stride = Scheduler.SchedulerTimeType.Stride
|
||||
|
||||
XCTAssertEqual(Stride.seconds(1.2).magnitude, 1_200_000_000)
|
||||
XCTAssertEqual(Stride.seconds(2).magnitude, 2_000_000_000)
|
||||
XCTAssertEqual(Stride.milliseconds(2).magnitude, 2_000_000)
|
||||
XCTAssertEqual(Stride.microseconds(2).magnitude, 2_000)
|
||||
XCTAssertEqual(Stride.nanoseconds(2).magnitude, 2)
|
||||
|
||||
#if arch(x86_64) || arch(arm64) || arch(s390x) || arch(powerpc64) || arch(powerpc64le)
|
||||
// 64-bit platforms
|
||||
XCTAssertEqual(
|
||||
Stride.seconds(Double(Int.max) / 1_000_000_000 - 1).magnitude,
|
||||
9223372035854776320
|
||||
)
|
||||
#elseif arch(i386) || arch(arm)
|
||||
// 32-bit platforms
|
||||
XCTAssertEqual(
|
||||
Stride.seconds(Double(Int.max) / 1_000_000_000).magnitude,
|
||||
.max
|
||||
)
|
||||
#else
|
||||
#error("This architecture isn't known. Add it to the 32-bit or 64-bit line.")
|
||||
#endif
|
||||
|
||||
XCTAssertEqual(Stride.seconds(.max).magnitude, .max)
|
||||
XCTAssertEqual(Stride.milliseconds(.max).magnitude, .max)
|
||||
XCTAssertEqual(Stride.microseconds(.max).magnitude, .max)
|
||||
XCTAssertEqual(Stride.nanoseconds(.max).magnitude, .max)
|
||||
|
||||
XCTAssertEqual((1.2 as Stride).magnitude, 1_200_000_000)
|
||||
XCTAssertEqual((2 as Stride).magnitude, 2_000_000_000)
|
||||
|
||||
@@ -126,17 +257,33 @@ final class DispatchQueueSchedulerTests: XCTestCase {
|
||||
XCTAssertEqual(Stride(exactly: 871 as UInt64)?.magnitude, 871)
|
||||
}
|
||||
|
||||
func testStrideComparable() {
|
||||
typealias Stride = Scheduler.SchedulerTimeType.Stride
|
||||
func testStrideFromTooMuchSecondsCrashes() {
|
||||
assertCrashes {
|
||||
#if arch(x86_64) || arch(arm64) || arch(s390x) || arch(powerpc64) || arch(powerpc64le)
|
||||
// 64-bit platforms
|
||||
XCTAssertGreaterThan(
|
||||
Stride.seconds(Double(Int.max) / 1_000_000_000).magnitude,
|
||||
.max
|
||||
)
|
||||
#elseif arch(i386) || arch(arm)
|
||||
// 32-bit platforms
|
||||
XCTAssertGreaterThan(
|
||||
Stride.seconds(Double(Int.max) / 1_000_000_000 + 1).magnitude,
|
||||
.max
|
||||
)
|
||||
#else
|
||||
#error("This architecture isn't known. Add it to the 32-bit or 64-bit line.")
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
func testStrideComparable() {
|
||||
XCTAssertLessThan(Stride.nanoseconds(1), .nanoseconds(2))
|
||||
XCTAssertGreaterThan(Stride.nanoseconds(-2), .microseconds(-10))
|
||||
XCTAssertLessThan(Stride.milliseconds(2), .seconds(2))
|
||||
}
|
||||
|
||||
func testStrideMultiplication() {
|
||||
typealias Stride = Scheduler.SchedulerTimeType.Stride
|
||||
|
||||
XCTAssertEqual((Stride.nanoseconds(0) * .nanoseconds(61346)).magnitude, 0)
|
||||
XCTAssertEqual((Stride.nanoseconds(61346) * .nanoseconds(0)).magnitude, 0)
|
||||
XCTAssertEqual((Stride.nanoseconds(18) * .nanoseconds(1)).magnitude, 18)
|
||||
@@ -196,8 +343,6 @@ final class DispatchQueueSchedulerTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testStrideAddition() {
|
||||
typealias Stride = Scheduler.SchedulerTimeType.Stride
|
||||
|
||||
XCTAssertEqual((Stride.nanoseconds(0) + .microseconds(2)).magnitude, 2000)
|
||||
XCTAssertEqual((Stride.nanoseconds(2) + .microseconds(0)).magnitude, 2)
|
||||
XCTAssertEqual((Stride.nanoseconds(7) + .nanoseconds(12)).magnitude, 19)
|
||||
@@ -243,8 +388,6 @@ final class DispatchQueueSchedulerTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testStrideSubtraction() {
|
||||
typealias Stride = Scheduler.SchedulerTimeType.Stride
|
||||
|
||||
XCTAssertEqual((Stride.nanoseconds(0) - .microseconds(2)).magnitude, -2000)
|
||||
XCTAssertEqual((Stride.nanoseconds(2) - .microseconds(0)).magnitude, 2)
|
||||
XCTAssertEqual((Stride.nanoseconds(7) - .nanoseconds(12)).magnitude, -5)
|
||||
@@ -290,8 +433,6 @@ final class DispatchQueueSchedulerTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testStrideCodable() throws {
|
||||
typealias Stride = Scheduler.SchedulerTimeType.Stride
|
||||
|
||||
let encoder = JSONEncoder()
|
||||
let decoder = JSONDecoder()
|
||||
|
||||
@@ -429,6 +570,9 @@ private let backgroundScheduler = DispatchQueue.global(qos: .background).ocombin
|
||||
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private typealias Stride = Scheduler.SchedulerTimeType.Stride
|
||||
|
||||
private struct KeyedWrapper<Value: Codable & Equatable>: Codable, Equatable {
|
||||
let value: Value
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
//
|
||||
// TopLevelDecoder+Extensions.swift
|
||||
//
|
||||
//
|
||||
// Created by Joseph Spadafora on 6/29/19.
|
||||
//
|
||||
|
||||
#if !OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Foundation
|
||||
import OpenCombine
|
||||
|
||||
extension JSONDecoder: TopLevelDecoder {}
|
||||
extension JSONEncoder: TopLevelEncoder {}
|
||||
|
||||
extension PropertyListDecoder: TopLevelDecoder {}
|
||||
extension PropertyListEncoder: TopLevelEncoder {}
|
||||
#endif
|
||||
@@ -0,0 +1,63 @@
|
||||
//
|
||||
// JSONDecoderTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 10.12.2019.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
import OpenCombineFoundation
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class JSONDecoderTests: XCTestCase {
|
||||
func testSuccessfullyDecode() {
|
||||
let decoder = JSONDecoder()
|
||||
let input = #"[{"success":true}]"#
|
||||
var actualOutput: [Subscribers.Completion<TestingError>]?
|
||||
var actualCompletion: Subscribers.Completion<Error>?
|
||||
let cancellable = Just(input)
|
||||
.map { Data($0.utf8) }
|
||||
.decode(type: [Subscribers.Completion<TestingError>].self, decoder: decoder)
|
||||
.sink(receiveCompletion: { actualCompletion = $0 },
|
||||
receiveValue: { actualOutput = $0 })
|
||||
switch actualCompletion {
|
||||
case .finished?:
|
||||
XCTAssertEqual(actualOutput, [.finished])
|
||||
case .failure(let error)?:
|
||||
XCTFail("Unexpected failure received: \(error)")
|
||||
case nil:
|
||||
XCTFail("Expected completion")
|
||||
}
|
||||
cancellable.cancel()
|
||||
}
|
||||
|
||||
func testDecodingFailure() {
|
||||
let decoder = JSONDecoder()
|
||||
let input = #"{"a":1,"b":2}"#
|
||||
var actualOutput: [Int]?
|
||||
var actualCompletion: Subscribers.Completion<Error>?
|
||||
let cancellable = Just(input)
|
||||
.map { Data($0.utf8) }
|
||||
.decode(type: [Int].self, decoder: decoder)
|
||||
.sink(receiveCompletion: { actualCompletion = $0 },
|
||||
receiveValue: { actualOutput = $0 })
|
||||
switch actualCompletion {
|
||||
case .finished?:
|
||||
XCTFail("Unexpected success")
|
||||
case .failure(DecodingError.typeMismatch)?:
|
||||
XCTAssertNil(actualOutput)
|
||||
case .failure(let error)?:
|
||||
XCTFail("Unexpected failure received: \(error)")
|
||||
case nil:
|
||||
XCTFail("Expected completion")
|
||||
}
|
||||
cancellable.cancel()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
//
|
||||
// JSONEncoderTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 10.12.2019.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
import OpenCombineFoundation
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class JSONEncoderTests: XCTestCase {
|
||||
|
||||
func testSuccessfullyEncode() {
|
||||
let encoder = JSONEncoder()
|
||||
let input = [Subscribers.Completion<TestingError>.finished]
|
||||
var actualOutput: String?
|
||||
var actualCompletion: Subscribers.Completion<Error>?
|
||||
|
||||
let cancellable = Just(input)
|
||||
.encode(encoder: encoder)
|
||||
.map { String(decoding: $0, as: UTF8.self) }
|
||||
.sink(receiveCompletion: { actualCompletion = $0 },
|
||||
receiveValue: { actualOutput = $0 })
|
||||
|
||||
switch actualCompletion {
|
||||
case .finished?:
|
||||
XCTAssertEqual(actualOutput, #"[{"success":true}]"#)
|
||||
case .failure(let error)?:
|
||||
XCTFail("Unexpected failure received: \(error)")
|
||||
case nil:
|
||||
XCTFail("Expected completion")
|
||||
}
|
||||
cancellable.cancel()
|
||||
}
|
||||
|
||||
func testEncodingFailure() {
|
||||
let encoder = JSONEncoder()
|
||||
let input = Double.nan
|
||||
var actualOutput: String?
|
||||
var actualCompletion: Subscribers.Completion<Error>?
|
||||
let cancellable = Just(input)
|
||||
.encode(encoder: encoder)
|
||||
.map { String(decoding: $0, as: UTF8.self) }
|
||||
.sink(receiveCompletion: { actualCompletion = $0 },
|
||||
receiveValue: { actualOutput = $0 })
|
||||
|
||||
switch actualCompletion {
|
||||
case .finished?:
|
||||
XCTFail("Unexpected success")
|
||||
case .failure(EncodingError.invalidValue)?:
|
||||
XCTAssertNil(actualOutput)
|
||||
case .failure(let error)?:
|
||||
XCTFail("Unexpected failure received: \(error)")
|
||||
case nil:
|
||||
XCTFail("Expected completion")
|
||||
}
|
||||
cancellable.cancel()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,625 @@
|
||||
//
|
||||
// NotificationCenterTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 10.12.2019.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
import OpenCombineFoundation
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class NotificationCenterTests: XCTestCase {
|
||||
|
||||
func testRequestingDemand() {
|
||||
|
||||
let initialDemands: [Subscribers.Demand?] = [
|
||||
nil,
|
||||
.max(1),
|
||||
.max(2),
|
||||
.max(10),
|
||||
.unlimited
|
||||
]
|
||||
|
||||
let subsequentDemands: [[Subscribers.Demand]] = [
|
||||
Array(repeating: .max(0), count: 5),
|
||||
Array(repeating: .max(1), count: 10),
|
||||
[.max(1), .max(0), .max(1), .max(0)],
|
||||
[.max(0), .max(1), .max(2)],
|
||||
[.unlimited, .max(1)]
|
||||
]
|
||||
|
||||
var numberOfInputsHistory: [Int] = []
|
||||
let expectedNumberOfInputsHistory = [
|
||||
0, 0, 0, 0, 0, 1, 11, 2, 1, 20, 2, 12, 4, 5, 20, 10, 20, 12, 13, 20, 20,
|
||||
20, 20, 20, 20
|
||||
]
|
||||
|
||||
for initialDemand in initialDemands {
|
||||
for subsequentDemand in subsequentDemands {
|
||||
|
||||
var i = 0
|
||||
|
||||
let center = TestNotificationCenter()
|
||||
let name = Notification.Name(rawValue: "testName")
|
||||
let publisher = makePublisher(center, for: name, object: nil)
|
||||
|
||||
let subscriber = TrackingSubscriberBase<Notification, Never>(
|
||||
receiveSubscription: { initialDemand.map($0.request) },
|
||||
receiveValue: { _ in
|
||||
defer { i += 1 }
|
||||
return i < subsequentDemand.endIndex ? subsequentDemand[i] : .none
|
||||
}
|
||||
)
|
||||
|
||||
XCTAssertEqual(subscriber.subscriptions.count, 0)
|
||||
XCTAssertEqual(subscriber.inputs.count, 0)
|
||||
XCTAssertEqual(subscriber.completions.count, 0)
|
||||
|
||||
publisher.subscribe(subscriber)
|
||||
|
||||
XCTAssertEqual(subscriber.subscriptions.count, 1)
|
||||
XCTAssertEqual(subscriber.inputs.count, 0)
|
||||
XCTAssertEqual(subscriber.completions.count, 0)
|
||||
|
||||
for _ in 0..<20 {
|
||||
center.post(name: name, object: TestObject.two)
|
||||
}
|
||||
|
||||
XCTAssertEqual(subscriber.subscriptions.count, 1)
|
||||
XCTAssertEqual(subscriber.completions.count, 0)
|
||||
|
||||
numberOfInputsHistory.append(subscriber.inputs.count)
|
||||
}
|
||||
}
|
||||
|
||||
XCTAssertEqual(numberOfInputsHistory, expectedNumberOfInputsHistory)
|
||||
}
|
||||
|
||||
func testBasicBehavior() throws {
|
||||
let center = TestNotificationCenter()
|
||||
let name = Notification.Name(rawValue: "testName")
|
||||
let publisher = makePublisher(center, for: name, object: TestObject.one)
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriberBase<Notification, Never>(
|
||||
receiveSubscription: { downstreamSubscription = $0 }
|
||||
)
|
||||
|
||||
XCTAssertEqual(center.history, [])
|
||||
|
||||
publisher.subscribe(tracking)
|
||||
|
||||
XCTAssertEqual(center.history,
|
||||
[.addObserver(name, TestObject.one, nil)])
|
||||
XCTAssertEqual(tracking.history,
|
||||
[.subscription("NotificationCenter Observer")])
|
||||
|
||||
let note = Notification(name: name, object: TestObject.one, userInfo: nil)
|
||||
center.post(note)
|
||||
|
||||
XCTAssertEqual(center.history,
|
||||
[.addObserver(name, TestObject.one, nil),
|
||||
.postNotification(note)])
|
||||
XCTAssertEqual(tracking.history,
|
||||
[.subscription("NotificationCenter Observer")])
|
||||
|
||||
try XCTUnwrap(downstreamSubscription).request(.max(3))
|
||||
|
||||
center.post(note)
|
||||
|
||||
XCTAssertEqual(center.history,
|
||||
[.addObserver(name, TestObject.one, nil),
|
||||
.postNotification(note),
|
||||
.postNotification(note)])
|
||||
XCTAssertEqual(tracking.history,
|
||||
[.subscription("NotificationCenter Observer"),
|
||||
.value(note)])
|
||||
|
||||
let unrelatedNote1 = Notification(name: Notification.Name("unrelatedNote1"),
|
||||
object: TestObject.one,
|
||||
userInfo: nil)
|
||||
center.post(unrelatedNote1)
|
||||
|
||||
let unrelatedNote2 = Notification(name: name,
|
||||
object: TestObject.two,
|
||||
userInfo: nil)
|
||||
center.post(unrelatedNote2)
|
||||
center.post(name: name, object: nil)
|
||||
center.post(name: name, object: nil)
|
||||
center.post(name: name, object: TestObject.one)
|
||||
center.post(name: name, object: TestObject.one)
|
||||
|
||||
XCTAssertEqual(center.history,
|
||||
[.addObserver(name, TestObject.one, nil),
|
||||
.postNotification(note),
|
||||
.postNotification(note),
|
||||
.postNotification(unrelatedNote1),
|
||||
.postNotification(unrelatedNote2),
|
||||
.postNotificationWithName(name, nil, nil),
|
||||
.postNotification(Notification(name: name)),
|
||||
.postNotificationWithName(name, nil, nil),
|
||||
.postNotification(Notification(name: name)),
|
||||
.postNotificationWithName(name, TestObject.one, nil),
|
||||
.postNotification(Notification(name: name,
|
||||
object: TestObject.one)),
|
||||
.postNotificationWithName(name, TestObject.one, nil),
|
||||
.postNotification(Notification(name: name,
|
||||
object: TestObject.one))])
|
||||
XCTAssertEqual(tracking.history,
|
||||
[.subscription("NotificationCenter Observer"),
|
||||
.value(note),
|
||||
.value(unrelatedNote1),
|
||||
.value(unrelatedNote2)])
|
||||
|
||||
try XCTUnwrap(downstreamSubscription).request(.unlimited)
|
||||
|
||||
center.post(note)
|
||||
|
||||
try XCTUnwrap(downstreamSubscription).cancel()
|
||||
|
||||
center.post(note)
|
||||
|
||||
XCTAssertEqual(center.history,
|
||||
[.addObserver(name, TestObject.one, nil),
|
||||
.postNotification(note),
|
||||
.postNotification(note),
|
||||
.postNotification(unrelatedNote1),
|
||||
.postNotification(unrelatedNote2),
|
||||
.postNotificationWithName(name, nil, nil),
|
||||
.postNotification(Notification(name: name)),
|
||||
.postNotificationWithName(name, nil, nil),
|
||||
.postNotification(Notification(name: name)),
|
||||
.postNotificationWithName(name, TestObject.one, nil),
|
||||
.postNotification(Notification(name: name,
|
||||
object: TestObject.one)),
|
||||
.postNotificationWithName(name, TestObject.one, nil),
|
||||
.postNotification(Notification(name: name,
|
||||
object: TestObject.one)),
|
||||
.postNotification(note),
|
||||
.removeObserver,
|
||||
.removeObserverForName(nil, nil),
|
||||
.postNotification(note)])
|
||||
XCTAssertEqual(tracking.history,
|
||||
[.subscription("NotificationCenter Observer"),
|
||||
.value(note),
|
||||
.value(unrelatedNote1),
|
||||
.value(unrelatedNote2),
|
||||
.value(note)])
|
||||
}
|
||||
|
||||
func testBasicBehaviorNilObject() throws {
|
||||
let center = TestNotificationCenter()
|
||||
let name = Notification.Name(rawValue: "testName")
|
||||
let publisher = makePublisher(center, for: name, object: nil)
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriberBase<Notification, Never>(
|
||||
receiveSubscription: { downstreamSubscription = $0 }
|
||||
)
|
||||
|
||||
XCTAssertEqual(center.history, [])
|
||||
|
||||
publisher.subscribe(tracking)
|
||||
|
||||
XCTAssertEqual(center.history,
|
||||
[.addObserver(name, nil, nil)])
|
||||
XCTAssertEqual(tracking.history,
|
||||
[.subscription("NotificationCenter Observer")])
|
||||
|
||||
let note = Notification(name: name, object: TestObject.one, userInfo: nil)
|
||||
center.post(note)
|
||||
|
||||
XCTAssertEqual(center.history,
|
||||
[.addObserver(name, nil, nil),
|
||||
.postNotification(note)])
|
||||
XCTAssertEqual(tracking.history,
|
||||
[.subscription("NotificationCenter Observer")])
|
||||
|
||||
try XCTUnwrap(downstreamSubscription).request(.max(3))
|
||||
|
||||
center.post(note)
|
||||
|
||||
XCTAssertEqual(center.history,
|
||||
[.addObserver(name, nil, nil),
|
||||
.postNotification(note),
|
||||
.postNotification(note)])
|
||||
XCTAssertEqual(tracking.history,
|
||||
[.subscription("NotificationCenter Observer"),
|
||||
.value(note)])
|
||||
|
||||
let unrelatedNote = Notification(name: Notification.Name("unrelatedNote"),
|
||||
object: TestObject.one,
|
||||
userInfo: nil)
|
||||
center.post(unrelatedNote)
|
||||
center.post(name: name, object: nil)
|
||||
center.post(name: name, object: nil)
|
||||
center.post(name: name, object: TestObject.one)
|
||||
center.post(name: name, object: TestObject.one)
|
||||
|
||||
XCTAssertEqual(center.history,
|
||||
[.addObserver(name, nil, nil),
|
||||
.postNotification(note),
|
||||
.postNotification(note),
|
||||
.postNotification(unrelatedNote),
|
||||
.postNotificationWithName(name, nil, nil),
|
||||
.postNotification(Notification(name: name)),
|
||||
.postNotificationWithName(name, nil, nil),
|
||||
.postNotification(Notification(name: name)),
|
||||
.postNotificationWithName(name, TestObject.one, nil),
|
||||
.postNotification(note),
|
||||
.postNotificationWithName(name, TestObject.one, nil),
|
||||
.postNotification(note)])
|
||||
XCTAssertEqual(tracking.history,
|
||||
[.subscription("NotificationCenter Observer"),
|
||||
.value(note),
|
||||
.value(unrelatedNote),
|
||||
.value(Notification(name: name))])
|
||||
|
||||
try XCTUnwrap(downstreamSubscription).request(.unlimited)
|
||||
|
||||
center.post(note)
|
||||
|
||||
try XCTUnwrap(downstreamSubscription).cancel()
|
||||
|
||||
center.post(note)
|
||||
|
||||
XCTAssertEqual(center.history,
|
||||
[.addObserver(name, nil, nil),
|
||||
.postNotification(note),
|
||||
.postNotification(note),
|
||||
.postNotification(unrelatedNote),
|
||||
.postNotificationWithName(name, nil, nil),
|
||||
.postNotification(Notification(name: name)),
|
||||
.postNotificationWithName(name, nil, nil),
|
||||
.postNotification(Notification(name: name)),
|
||||
.postNotificationWithName(name, TestObject.one, nil),
|
||||
.postNotification(note),
|
||||
.postNotificationWithName(name, TestObject.one, nil),
|
||||
.postNotification(note),
|
||||
.postNotification(note),
|
||||
.removeObserver,
|
||||
.removeObserverForName(nil, nil),
|
||||
.postNotification(note)])
|
||||
XCTAssertEqual(tracking.history,
|
||||
[.subscription("NotificationCenter Observer"),
|
||||
.value(note),
|
||||
.value(unrelatedNote),
|
||||
.value(Notification(name: name)),
|
||||
.value(note)])
|
||||
}
|
||||
|
||||
func testRecursivelyReceiveValue() throws {
|
||||
let center = TestNotificationCenter()
|
||||
let name = Notification.Name(rawValue: "testName")
|
||||
let publisher = makePublisher(center, for: name, object: nil)
|
||||
let tracking = TrackingSubscriberBase<Notification, Never>(
|
||||
receiveSubscription: { $0.request(.max(3)) },
|
||||
receiveValue: { _ in .unlimited }
|
||||
)
|
||||
publisher.subscribe(tracking)
|
||||
|
||||
let note = Notification(name: name)
|
||||
var recursionCounter = 7
|
||||
tracking.onValue = { _ in
|
||||
if recursionCounter == 0 { return }
|
||||
recursionCounter -= 1
|
||||
center.post(note)
|
||||
}
|
||||
|
||||
center.post(note)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("NotificationCenter Observer"),
|
||||
.value(note),
|
||||
.value(note),
|
||||
.value(note)])
|
||||
|
||||
center.post(note)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("NotificationCenter Observer"),
|
||||
.value(note),
|
||||
.value(note),
|
||||
.value(note),
|
||||
.value(note),
|
||||
.value(note),
|
||||
.value(note),
|
||||
.value(note),
|
||||
.value(note)])
|
||||
}
|
||||
|
||||
func testCancelAlreadyCancelled() throws {
|
||||
let center = TestNotificationCenter()
|
||||
let name = Notification.Name(rawValue: "testName")
|
||||
let publisher = makePublisher(center, for: name, object: nil)
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriberBase<Notification, Never>(
|
||||
receiveSubscription: { downstreamSubscription = $0 }
|
||||
)
|
||||
publisher.subscribe(tracking)
|
||||
|
||||
try XCTUnwrap(downstreamSubscription).cancel()
|
||||
try XCTUnwrap(downstreamSubscription).cancel()
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("NotificationCenter Observer")])
|
||||
XCTAssertEqual(center.history,
|
||||
[.addObserver(name, nil, nil),
|
||||
.removeObserver,
|
||||
.removeObserverForName(nil, nil)])
|
||||
}
|
||||
|
||||
func testCancellingReleasesNotificationCenter() throws {
|
||||
var centerDestroyed = false
|
||||
var downstreamSubscription: Subscription?
|
||||
do {
|
||||
let center = TestNotificationCenter()
|
||||
center.onDeinit = { centerDestroyed = true }
|
||||
let name = Notification.Name(rawValue: "testName")
|
||||
let publisher = makePublisher(center, for: name, object: nil)
|
||||
let tracking = TrackingSubscriberBase<Notification, Never>(
|
||||
receiveSubscription: { downstreamSubscription = $0 }
|
||||
)
|
||||
publisher.subscribe(tracking)
|
||||
}
|
||||
XCTAssertFalse(centerDestroyed)
|
||||
try XCTUnwrap(downstreamSubscription).cancel()
|
||||
XCTAssertTrue(centerDestroyed)
|
||||
}
|
||||
|
||||
func testWeakCaptureWhenAddingObserver() {
|
||||
let center = TestNotificationCenter()
|
||||
let name = Notification.Name("testName")
|
||||
var value: Notification?
|
||||
do {
|
||||
let publisher = makePublisher(center, for: name, object: nil)
|
||||
let tracking = TrackingSubscriberBase<Notification, Never>(
|
||||
receiveSubscription: { $0.request(.max(1)) },
|
||||
receiveValue: { value = $0; return .none }
|
||||
)
|
||||
publisher.subscribe(tracking)
|
||||
tracking.clearHistory() // Release the subscription
|
||||
}
|
||||
center.post(name: name, object: nil)
|
||||
XCTAssertEqual(center.history,
|
||||
[.addObserver(name, nil, nil),
|
||||
.postNotificationWithName(name, nil, nil),
|
||||
.postNotification(Notification(name: name))])
|
||||
XCTAssertNil(value)
|
||||
}
|
||||
|
||||
func testZeroDemand() throws {
|
||||
let center = TestNotificationCenter()
|
||||
let name = Notification.Name(rawValue: "testName")
|
||||
let publisher = makePublisher(center, for: name, object: nil)
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriberBase<Notification, Never>(
|
||||
receiveSubscription: { downstreamSubscription = $0 }
|
||||
)
|
||||
publisher.subscribe(tracking)
|
||||
try XCTUnwrap(downstreamSubscription).request(.none)
|
||||
|
||||
XCTAssertEqual(center.history,
|
||||
[.addObserver(name, nil, nil)])
|
||||
XCTAssertEqual(tracking.history, [.subscription("NotificationCenter Observer")])
|
||||
}
|
||||
|
||||
func testNotificationCenterSubscriptionReflection() throws {
|
||||
let center = TestNotificationCenter()
|
||||
let name = Notification.Name(rawValue: "testName")
|
||||
let object = TestObject.one
|
||||
let publisher = makePublisher(center, for: name, object: object)
|
||||
|
||||
try testSubscriptionReflection(
|
||||
description: "NotificationCenter Observer",
|
||||
customMirror: expectedChildren(
|
||||
("center", .matches(String(describing: Optional(center)))),
|
||||
("name", .contains(String(describing: name))),
|
||||
("object", .matches(String(describing: Optional(object)))),
|
||||
("demand", "max(0)")
|
||||
),
|
||||
playgroundDescription: "NotificationCenter Observer",
|
||||
sut: publisher
|
||||
)
|
||||
}
|
||||
|
||||
func testEquatable() {
|
||||
let center1 = NotificationCenter()
|
||||
let center2 = NotificationCenter()
|
||||
let name1 = Notification.Name(rawValue: "abcdefg")
|
||||
let name2 = Notification.Name(rawValue: "1234567")
|
||||
let object1 = TestObject.one
|
||||
let object2 = TestObject.two
|
||||
|
||||
XCTAssertEqual(makePublisher(center1, for: name1, object: object1),
|
||||
makePublisher(center1, for: name1, object: object1))
|
||||
XCTAssertEqual(makePublisher(center2, for: name2, object: object2),
|
||||
makePublisher(center2, for: name2, object: object2))
|
||||
XCTAssertEqual(makePublisher(center1, for: name1, object: nil),
|
||||
makePublisher(center1, for: name1, object: nil))
|
||||
XCTAssertNotEqual(makePublisher(center1, for: name1, object: object1),
|
||||
makePublisher(center1, for: name1, object: nil))
|
||||
XCTAssertNotEqual(makePublisher(center1, for: name1, object: nil),
|
||||
makePublisher(center1, for: name1, object: object2))
|
||||
XCTAssertNotEqual(makePublisher(center1, for: name1, object: object1),
|
||||
makePublisher(center1, for: name1, object: object2))
|
||||
XCTAssertNotEqual(makePublisher(center1, for: name1, object: object1),
|
||||
makePublisher(center1, for: name2, object: object1))
|
||||
XCTAssertNotEqual(makePublisher(center1, for: name1, object: object1),
|
||||
makePublisher(center2, for: name1, object: object1))
|
||||
}
|
||||
}
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST || !canImport(Combine)
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private func makePublisher(
|
||||
_ center: NotificationCenter,
|
||||
for name: Notification.Name,
|
||||
object: AnyObject?
|
||||
) -> NotificationCenter.Publisher {
|
||||
return center.publisher(for: name, object: object)
|
||||
}
|
||||
#else
|
||||
private func makePublisher(
|
||||
_ center: NotificationCenter,
|
||||
for name: Notification.Name,
|
||||
object: AnyObject?
|
||||
) -> NotificationCenter.OCombine.Publisher {
|
||||
return center.ocombine.publisher(for: name, object: object)
|
||||
}
|
||||
#endif
|
||||
|
||||
/// A simple mock notification center that always sends notifications to **all**
|
||||
/// observers in non-thread safe manner.
|
||||
private final class TestNotificationCenter: NotificationCenter {
|
||||
|
||||
enum Event {
|
||||
case postNotificationWithName(Notification.Name, Any?, [AnyHashable : Any]?)
|
||||
case postNotification(Notification)
|
||||
case addObserver(Notification.Name?, Any?, OperationQueue?)
|
||||
case removeObserver
|
||||
case removeObserverForName(Notification.Name?, Any?)
|
||||
}
|
||||
|
||||
private final class Observation {
|
||||
let callback: (Notification) -> Void
|
||||
|
||||
init(callback: @escaping (Notification) -> Void) {
|
||||
self.callback = callback
|
||||
}
|
||||
}
|
||||
|
||||
private final class Token: NSObject {
|
||||
weak var observation: Observation?
|
||||
|
||||
init(observer: TestNotificationCenter.Observation) {
|
||||
self.observation = observer
|
||||
}
|
||||
}
|
||||
|
||||
private(set) var history = [Event]()
|
||||
|
||||
private var observations: [Observation] = []
|
||||
|
||||
var onDeinit: (() -> Void)?
|
||||
|
||||
deinit {
|
||||
onDeinit?()
|
||||
}
|
||||
|
||||
override func post(name aName: Notification.Name,
|
||||
object anObject: Any?,
|
||||
userInfo aUserInfo: [AnyHashable : Any]? = nil) {
|
||||
history.append(.postNotificationWithName(aName, anObject, aUserInfo))
|
||||
let notification = Notification(name: aName,
|
||||
object: anObject,
|
||||
userInfo: aUserInfo)
|
||||
post(notification)
|
||||
}
|
||||
|
||||
override func post(_ notification: Notification) {
|
||||
history.append(.postNotification(notification))
|
||||
for observation in observations {
|
||||
observation.callback(notification)
|
||||
}
|
||||
}
|
||||
|
||||
override func addObserver(
|
||||
forName name: NSNotification.Name?,
|
||||
object obj: Any?,
|
||||
queue: OperationQueue?,
|
||||
using block: @escaping (Notification) -> Void
|
||||
) -> NSObjectProtocol {
|
||||
history.append(.addObserver(name, obj, queue))
|
||||
let observer = Observation(callback: block)
|
||||
observations.append(observer)
|
||||
return Token(observer: observer)
|
||||
}
|
||||
|
||||
override func removeObserver(_ observer: Any) {
|
||||
history.append(.removeObserver)
|
||||
removeObserver(observer, name: nil, object: nil)
|
||||
}
|
||||
|
||||
override func removeObserver(_ observer: Any,
|
||||
name aName: NSNotification.Name?,
|
||||
object anObject: Any?) {
|
||||
history.append(.removeObserverForName(aName, anObject))
|
||||
guard let observer = observer as? Token else { return }
|
||||
observations.removeAll { $0 === observer.observation }
|
||||
}
|
||||
}
|
||||
|
||||
private final class TestObject: NSObject {
|
||||
|
||||
static let one = TestObject()
|
||||
|
||||
static let two = TestObject()
|
||||
}
|
||||
|
||||
extension TestNotificationCenter.Event: Equatable {
|
||||
fileprivate static func == (lhs: TestNotificationCenter.Event,
|
||||
rhs: TestNotificationCenter.Event) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case let (.postNotification(lhsNote), .postNotification(rhsNote)):
|
||||
return lhsNote == rhsNote
|
||||
case let (.postNotificationWithName(lhsName,
|
||||
lhsObject as TestObject?,
|
||||
lhsUserInfo),
|
||||
.postNotificationWithName(rhsName,
|
||||
rhsObject as TestObject?,
|
||||
rhsUserInfo)):
|
||||
return lhsName == rhsName &&
|
||||
lhsObject === rhsObject &&
|
||||
(lhsUserInfo == nil) == (rhsUserInfo == nil)
|
||||
case let (.addObserver(lhsName, lhsObject as TestObject?, lhsQueue),
|
||||
.addObserver(rhsName, rhsObject as TestObject?, rhsQueue)):
|
||||
return lhsName == rhsName &&
|
||||
lhsObject === rhsObject &&
|
||||
lhsQueue == rhsQueue
|
||||
case (.removeObserver, .removeObserver):
|
||||
return true
|
||||
case let (.removeObserverForName(lhsName, lhsObject as TestObject?),
|
||||
.removeObserverForName(rhsName, rhsObject as TestObject?)):
|
||||
return lhsName == rhsName && lhsObject === rhsObject
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension TestNotificationCenter.Event: CustomStringConvertible {
|
||||
var description: String {
|
||||
switch self {
|
||||
case let .postNotificationWithName(name, object, userInfo):
|
||||
return """
|
||||
.postNotificationWithName(\
|
||||
.init(rawValue: \"\(name.rawValue)\"), \
|
||||
\(object.map(String.init(describing:)) ?? "nil"), \
|
||||
\(userInfo.map(String.init(describing:)) ?? "nil"))
|
||||
"""
|
||||
case .postNotification:
|
||||
return ".postNotification(note)"
|
||||
case let .addObserver(name, object, queue):
|
||||
let nameDescription = name.map { ".init(rawValue: \($0.rawValue))" } ?? "nil"
|
||||
return """
|
||||
.addObserver(\
|
||||
\(nameDescription), \
|
||||
\(object.map(String.init(describing:)) ?? "nil"), \
|
||||
\(queue.map(String.init(describing:)) ?? "nil"))
|
||||
"""
|
||||
case .removeObserver:
|
||||
return ".removeObserver"
|
||||
case let .removeObserverForName(name, object):
|
||||
let nameDescription = name.map { ".init(rawValue: \($0.rawValue))" } ?? "nil"
|
||||
return """
|
||||
.removeObserverForName(\
|
||||
\(nameDescription), \
|
||||
\(object.map(String.init(describing:)) ?? "nil"))
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
//
|
||||
// PropertyListDecoderTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 10.12.2019.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
import OpenCombineFoundation
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class PropertyListDecoderTests: XCTestCase {
|
||||
func testSuccessfullyDecode() {
|
||||
let decoder = PropertyListDecoder()
|
||||
let input = """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" \
|
||||
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<array>
|
||||
\t<dict>
|
||||
\t\t<key>success</key>
|
||||
\t\t<true/>
|
||||
\t</dict>
|
||||
</array>
|
||||
</plist>
|
||||
|
||||
"""
|
||||
var actualOutput: [Subscribers.Completion<TestingError>]?
|
||||
var actualCompletion: Subscribers.Completion<Error>?
|
||||
let cancellable = Just(input)
|
||||
.map { Data($0.utf8) }
|
||||
.decode(type: [Subscribers.Completion<TestingError>].self, decoder: decoder)
|
||||
.sink(receiveCompletion: { actualCompletion = $0 },
|
||||
receiveValue: { actualOutput = $0 })
|
||||
switch actualCompletion {
|
||||
case .finished?:
|
||||
XCTAssertEqual(actualOutput, [.finished])
|
||||
case .failure(let error)?:
|
||||
XCTFail("Unexpected failure received: \(error)")
|
||||
case nil:
|
||||
XCTFail("Expected completion")
|
||||
}
|
||||
cancellable.cancel()
|
||||
}
|
||||
|
||||
func testDecodingFailure() {
|
||||
let decoder = PropertyListDecoder()
|
||||
let input = "000000"
|
||||
var actualOutput: [Int]?
|
||||
var actualCompletion: Subscribers.Completion<Error>?
|
||||
let cancellable = Just(input)
|
||||
.map { Data($0.utf8) }
|
||||
.decode(type: [Int].self, decoder: decoder)
|
||||
.sink(receiveCompletion: { actualCompletion = $0 },
|
||||
receiveValue: { actualOutput = $0 })
|
||||
switch actualCompletion {
|
||||
case .finished?:
|
||||
XCTFail("Unexpected success")
|
||||
case .failure(DecodingError.typeMismatch)?:
|
||||
XCTAssertNil(actualOutput)
|
||||
case .failure(let error)?:
|
||||
XCTFail("Unexpected failure received: \(error)")
|
||||
case nil:
|
||||
XCTFail("Expected completion")
|
||||
}
|
||||
cancellable.cancel()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
//
|
||||
// PropertyListEncoderTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 10.12.2019.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
import OpenCombineFoundation
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class PropertyListEncoderTests: XCTestCase {
|
||||
|
||||
func testSuccessfullyEncode() {
|
||||
let encoder = PropertyListEncoder()
|
||||
encoder.outputFormat = .xml
|
||||
let input = [Subscribers.Completion<TestingError>.finished]
|
||||
var actualOutput: String?
|
||||
var actualCompletion: Subscribers.Completion<Error>?
|
||||
|
||||
let cancellable = Just(input)
|
||||
.encode(encoder: encoder)
|
||||
.map { String(decoding: $0, as: UTF8.self) }
|
||||
.sink(receiveCompletion: { actualCompletion = $0 },
|
||||
receiveValue: { actualOutput = $0 })
|
||||
|
||||
switch actualCompletion {
|
||||
case .finished?:
|
||||
XCTAssertEqual(actualOutput, """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" \
|
||||
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<array>
|
||||
\t<dict>
|
||||
\t\t<key>success</key>
|
||||
\t\t<true/>
|
||||
\t</dict>
|
||||
</array>
|
||||
</plist>
|
||||
|
||||
""")
|
||||
case .failure(let error)?:
|
||||
XCTFail("Unexpected failure received: \(error)")
|
||||
case nil:
|
||||
XCTFail("Expected completion")
|
||||
}
|
||||
cancellable.cancel()
|
||||
}
|
||||
|
||||
func testEncodingFailure() {
|
||||
let encoder = PropertyListEncoder()
|
||||
let input = Double.nan
|
||||
var actualOutput: String?
|
||||
var actualCompletion: Subscribers.Completion<Error>?
|
||||
let cancellable = Just(input)
|
||||
.encode(encoder: encoder)
|
||||
.map { String(decoding: $0, as: UTF8.self) }
|
||||
.sink(receiveCompletion: { actualCompletion = $0 },
|
||||
receiveValue: { actualOutput = $0 })
|
||||
|
||||
switch actualCompletion {
|
||||
case .finished?:
|
||||
XCTFail("Unexpected success")
|
||||
case .failure(EncodingError.invalidValue)?:
|
||||
XCTAssertNil(actualOutput)
|
||||
case .failure(let error)?:
|
||||
XCTFail("Unexpected failure received: \(error)")
|
||||
case nil:
|
||||
XCTFail("Expected completion")
|
||||
}
|
||||
cancellable.cancel()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,511 @@
|
||||
//
|
||||
// RunLoopSchedulerTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 14.12.2019.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
import OpenCombineFoundation
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class 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(
|
||||
Date.distantFuture - 1024
|
||||
)
|
||||
|
||||
XCTAssertEqual(time1.distance(to: time2).timeInterval, 431)
|
||||
XCTAssertEqual(time2.distance(to: time1).timeInterval, -431)
|
||||
|
||||
XCTAssertEqual(time1.distance(to: distantFuture).timeInterval, 64_092_201_200)
|
||||
XCTAssertEqual(distantFuture.distance(to: time1).timeInterval, -64_092_201_200)
|
||||
XCTAssertEqual(time2.distance(to: distantFuture).timeInterval, 64_092_200_769)
|
||||
XCTAssertEqual(distantFuture.distance(to: time2).timeInterval, -64_092_200_769)
|
||||
|
||||
XCTAssertEqual(time1.distance(to: notSoDistantFuture).timeInterval,
|
||||
64_092_200_176)
|
||||
XCTAssertEqual(notSoDistantFuture.distance(to: time1).timeInterval,
|
||||
-64_092_200_176)
|
||||
XCTAssertEqual(time2.distance(to: notSoDistantFuture).timeInterval,
|
||||
64_092_199_745)
|
||||
XCTAssertEqual(notSoDistantFuture.distance(to: time2).timeInterval,
|
||||
-64_092_199_745)
|
||||
|
||||
XCTAssertEqual(distantFuture.distance(to: distantFuture).timeInterval,
|
||||
0)
|
||||
XCTAssertEqual(notSoDistantFuture.distance(to: notSoDistantFuture).timeInterval,
|
||||
0)
|
||||
}
|
||||
|
||||
func testSchedulerTimeTypeAdvanced() {
|
||||
let time =
|
||||
Scheduler.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)
|
||||
|
||||
XCTAssertEqual(time.advanced(by: stride1),
|
||||
.init(Date(timeIntervalSinceReferenceDate: 10431)))
|
||||
|
||||
XCTAssertEqual(time.advanced(by: stride2),
|
||||
.init(Date(timeIntervalSinceReferenceDate: 9780)))
|
||||
|
||||
#if arch(x86_64) || arch(arm64) || arch(s390x) || arch(powerpc64) || arch(powerpc64le)
|
||||
// 64-bit platforms
|
||||
XCTAssertEqual(time.advanced(by: .nanoseconds(.max)).date,
|
||||
Date(timeIntervalSinceReferenceDate: 9223382036.854776))
|
||||
|
||||
XCTAssertEqual(time.advanced(by: .seconds(.max)).date,
|
||||
Date(timeIntervalSinceReferenceDate: 9.223372036854786E+18))
|
||||
#elseif arch(i386) || arch(arm)
|
||||
// 32-bit platforms
|
||||
XCTAssertEqual(time.advanced(by: .nanoseconds(.max)).date,
|
||||
Date(timeIntervalSinceReferenceDate: 10002.147483647))
|
||||
|
||||
XCTAssertEqual(time.advanced(by: .seconds(.max)).date,
|
||||
Date(timeIntervalSinceReferenceDate: 2147493647))
|
||||
#else
|
||||
#error("This architecture isn't known. Add it to the 32-bit or 64-bit line.")
|
||||
#endif
|
||||
|
||||
XCTAssertEqual(beginningOfTime.advanced(by: .nanoseconds(-1000)).date,
|
||||
Date(timeIntervalSinceReferenceDate: 0.999999))
|
||||
|
||||
XCTAssertEqual(beginningOfTime.advanced(by: .seconds(-1000)).date,
|
||||
Date(timeIntervalSinceReferenceDate: -999.0))
|
||||
}
|
||||
|
||||
func testSchedulerTimeTypeEquatable() {
|
||||
let time1 =
|
||||
Scheduler.SchedulerTimeType(Date(timeIntervalSinceReferenceDate: 10000))
|
||||
let time2 =
|
||||
Scheduler.SchedulerTimeType(Date(timeIntervalSinceReferenceDate: 10000))
|
||||
let time3 =
|
||||
Scheduler.SchedulerTimeType(Date(timeIntervalSinceReferenceDate: 10001))
|
||||
|
||||
XCTAssertEqual(time1, time1)
|
||||
XCTAssertEqual(time2, time2)
|
||||
XCTAssertEqual(time3, time3)
|
||||
|
||||
XCTAssertEqual(time1, time2)
|
||||
XCTAssertEqual(time2, time1)
|
||||
XCTAssertNotEqual(time1, time3)
|
||||
XCTAssertNotEqual(time3, time1)
|
||||
}
|
||||
|
||||
func testSchedulerTimeTypeCodable() throws {
|
||||
let encoder = JSONEncoder()
|
||||
let decoder = JSONDecoder()
|
||||
|
||||
let time =
|
||||
Scheduler.SchedulerTimeType(Date(timeIntervalSinceReferenceDate: 1024.75))
|
||||
let encodedData = try encoder
|
||||
.encode(time)
|
||||
let encodedString = String(decoding: encodedData, as: UTF8.self)
|
||||
|
||||
XCTAssertEqual(encodedString,
|
||||
#"{"date":1024.75}"#)
|
||||
|
||||
let decodedTime = try decoder
|
||||
.decode(Scheduler.SchedulerTimeType.self, from: encodedData)
|
||||
|
||||
XCTAssertEqual(decodedTime, time)
|
||||
}
|
||||
|
||||
// MARK: - Scheduler.SchedulerTimeType.Stride
|
||||
|
||||
func testStrideToTimeInterval() {
|
||||
XCTAssertEqual(Stride.seconds(2).timeInterval, 2)
|
||||
XCTAssertEqual(Stride.seconds(2.2).timeInterval, 2.2)
|
||||
XCTAssertEqual(Stride.seconds(Double.infinity).timeInterval, .infinity)
|
||||
XCTAssertEqual(Stride.milliseconds(2).timeInterval, 0.002)
|
||||
XCTAssertEqual(Stride.microseconds(2).timeInterval, 2E-06)
|
||||
XCTAssertEqual(Stride.nanoseconds(2).timeInterval, 2E-09)
|
||||
#if arch(x86_64) || arch(arm64) || arch(s390x) || arch(powerpc64) || arch(powerpc64le)
|
||||
// 64-bit platforms
|
||||
XCTAssertEqual(Stride.seconds(Int.max).timeInterval, 9.223372036854776E+18)
|
||||
XCTAssertEqual(Stride.milliseconds(.max).timeInterval, 9.223372036854776E+15)
|
||||
XCTAssertEqual(Stride.microseconds(.max).timeInterval, 9223372036854.775)
|
||||
XCTAssertEqual(Stride.nanoseconds(.max).timeInterval, 9223372036.854776)
|
||||
#elseif arch(i386) || arch(arm)
|
||||
// 32-bit platforms
|
||||
XCTAssertEqual(Stride.seconds(Int.max).timeInterval, 2147483647)
|
||||
XCTAssertEqual(Stride.milliseconds(.max).timeInterval, 2147483.647)
|
||||
XCTAssertEqual(Stride.microseconds(.max).timeInterval, 2147.483647)
|
||||
XCTAssertEqual(Stride.nanoseconds(.max).timeInterval, 2.147483647)
|
||||
#else
|
||||
#error("This architecture isn't known. Add it to the 32-bit or 64-bit line.")
|
||||
#endif
|
||||
}
|
||||
|
||||
func testStrideFromTimeInterval() throws {
|
||||
XCTAssertEqual(Stride(2).magnitude, 2)
|
||||
XCTAssertEqual(Stride(2.2).magnitude, 2.2)
|
||||
XCTAssertEqual(Stride(.infinity).magnitude, .infinity)
|
||||
XCTAssertEqual(Stride(0.002).magnitude, 0.002)
|
||||
XCTAssertEqual(Stride(2E-06).magnitude, 2E-06)
|
||||
XCTAssertEqual(Stride(2E-09).magnitude, 2E-09)
|
||||
XCTAssertEqual(Stride(9.223372036854776E+18).magnitude, 9.223372036854776E+18)
|
||||
}
|
||||
|
||||
func testStrideFromNumericValue() {
|
||||
XCTAssertEqual((1.2 as Stride).magnitude, 1.2)
|
||||
XCTAssertEqual((2 as Stride).magnitude, 2)
|
||||
|
||||
XCTAssertNil(Stride(exactly: UInt64.max))
|
||||
XCTAssertEqual(Stride(exactly: 871 as UInt64)?.magnitude, 871)
|
||||
}
|
||||
|
||||
func testStrideComparable() {
|
||||
XCTAssertLessThan(Stride.nanoseconds(1), .nanoseconds(2))
|
||||
XCTAssertGreaterThan(Stride.nanoseconds(-2), .microseconds(-10))
|
||||
XCTAssertLessThan(Stride.milliseconds(2), .seconds(2))
|
||||
}
|
||||
|
||||
func testStrideMultiplication() {
|
||||
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)
|
||||
XCTAssertEqual((Stride.nanoseconds(18) * .microseconds(1)).magnitude, 1.8E-14)
|
||||
XCTAssertEqual((Stride.nanoseconds(1) * .nanoseconds(18)).magnitude, 1.8E-17)
|
||||
XCTAssertEqual((Stride.microseconds(1) * .nanoseconds(18)).magnitude, 1.8E-14)
|
||||
XCTAssertEqual((Stride.nanoseconds(15) * .nanoseconds(2)).magnitude, 3E-17)
|
||||
XCTAssertEqual((Stride.microseconds(-3) * .nanoseconds(10)).magnitude, -3E-14)
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(0)
|
||||
stride *= .nanoseconds(61346)
|
||||
XCTAssertEqual(stride.magnitude, 0)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(61346)
|
||||
stride *= .nanoseconds(0)
|
||||
XCTAssertEqual(stride.magnitude, 0)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(18)
|
||||
stride *= .nanoseconds(1)
|
||||
XCTAssertEqual(stride.magnitude, 1.8E-17)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(18)
|
||||
stride *= .microseconds(1)
|
||||
XCTAssertEqual(stride.magnitude, 1.8E-14)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(1)
|
||||
stride *= .nanoseconds(18)
|
||||
XCTAssertEqual(stride.magnitude, 1.8E-17)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.microseconds(1)
|
||||
stride *= .nanoseconds(18)
|
||||
XCTAssertEqual(stride.magnitude, 1.8E-14)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(15)
|
||||
stride *= .nanoseconds(2)
|
||||
XCTAssertEqual(stride.magnitude, 3E-17)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.microseconds(-3)
|
||||
stride *= .nanoseconds(10)
|
||||
XCTAssertEqual(stride.magnitude, -3E-14)
|
||||
}
|
||||
}
|
||||
|
||||
func testStrideAddition() {
|
||||
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,
|
||||
1.8999999999999998E-08)
|
||||
XCTAssertEqual((Stride.nanoseconds(12) + .nanoseconds(7)).magnitude,
|
||||
1.8999999999999998E-08)
|
||||
XCTAssertEqual((Stride.nanoseconds(7) + .nanoseconds(-12)).magnitude, -5E-09)
|
||||
XCTAssertEqual((Stride.nanoseconds(-12) + .nanoseconds(7)).magnitude, -5E-09)
|
||||
XCTAssertEqual((Stride.milliseconds(-12) + .seconds(7)).magnitude, 6.988)
|
||||
XCTAssertEqual((Stride.seconds(-12) + .milliseconds(7)).magnitude, -11.993)
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(0)
|
||||
stride += .microseconds(2)
|
||||
XCTAssertEqual(stride.magnitude, 2E-06)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(2)
|
||||
stride += .microseconds(0)
|
||||
XCTAssertEqual(stride.magnitude, 2E-09)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(7)
|
||||
stride += .nanoseconds(12)
|
||||
XCTAssertEqual(stride.magnitude, 1.8999999999999998E-08)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(12)
|
||||
stride += .nanoseconds(7)
|
||||
XCTAssertEqual(stride.magnitude, 1.8999999999999998E-08)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(7)
|
||||
stride += .nanoseconds(-12)
|
||||
XCTAssertEqual(stride.magnitude, -5E-09)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(-12)
|
||||
stride += .nanoseconds(7)
|
||||
XCTAssertEqual(stride.magnitude, -5E-09)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.seconds(-12)
|
||||
stride += .milliseconds(7)
|
||||
XCTAssertEqual(stride.magnitude, -11.993)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.milliseconds(-12)
|
||||
stride += .seconds(7)
|
||||
XCTAssertEqual(stride.magnitude, 6.988)
|
||||
}
|
||||
}
|
||||
|
||||
func testStrideSubtraction() {
|
||||
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)
|
||||
XCTAssertEqual((Stride.nanoseconds(12) - .nanoseconds(7)).magnitude, 5E-09)
|
||||
XCTAssertEqual((Stride.nanoseconds(7) - .nanoseconds(-12)).magnitude,
|
||||
1.8999999999999998E-08)
|
||||
XCTAssertEqual((Stride.nanoseconds(-12) - .nanoseconds(7)).magnitude,
|
||||
-1.8999999999999998E-08)
|
||||
XCTAssertEqual((Stride.seconds(-12) - .milliseconds(7)).magnitude, -12.007)
|
||||
XCTAssertEqual((Stride.milliseconds(-12) - .seconds(7)).magnitude, -7.012)
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(0)
|
||||
stride -= .microseconds(2)
|
||||
XCTAssertEqual(stride.magnitude, -2E-06)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(2)
|
||||
stride -= .microseconds(0)
|
||||
XCTAssertEqual(stride.magnitude, 2E-09)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(7)
|
||||
stride -= .nanoseconds(12)
|
||||
XCTAssertEqual(stride.magnitude, -5E-09)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(12)
|
||||
stride -= .nanoseconds(7)
|
||||
XCTAssertEqual(stride.magnitude, 5E-09)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(7)
|
||||
stride -= .nanoseconds(-12)
|
||||
XCTAssertEqual(stride.magnitude, 1.8999999999999998E-08)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(-12)
|
||||
stride -= .nanoseconds(7)
|
||||
XCTAssertEqual(stride.magnitude, -1.8999999999999998E-08)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.seconds(-12)
|
||||
stride -= .milliseconds(7)
|
||||
XCTAssertEqual(stride.magnitude, -12.007)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.milliseconds(-12)
|
||||
stride -= .seconds(7)
|
||||
XCTAssertEqual(stride.magnitude, -7.012)
|
||||
}
|
||||
}
|
||||
|
||||
func testStrideCodable() throws {
|
||||
let encoder = JSONEncoder()
|
||||
let decoder = JSONDecoder()
|
||||
|
||||
let stride = Stride.seconds(1024.5)
|
||||
let encodedData = try encoder.encode(stride)
|
||||
let encodedString = String(decoding: encodedData, as: UTF8.self)
|
||||
|
||||
XCTAssertEqual(encodedString, #"{"magnitude":1024.5}"#)
|
||||
|
||||
let decodedStride = try decoder
|
||||
.decode(Stride.self, from: encodedData)
|
||||
|
||||
XCTAssertEqual(decodedStride, stride)
|
||||
}
|
||||
|
||||
// MARK: - Scheduler
|
||||
|
||||
func testScheduleActionOnceNow() {
|
||||
let mainRunLoop = RunLoop.main
|
||||
let now = Date()
|
||||
var actualDate = Date.distantPast
|
||||
executeOnBackgroundThread {
|
||||
makeScheduler(mainRunLoop).schedule {
|
||||
XCTAssertTrue(Thread.isMainThread)
|
||||
actualDate = Date()
|
||||
RunLoop.current.run(until: Date() + 0.01)
|
||||
}
|
||||
}
|
||||
XCTAssertEqual(actualDate, .distantPast)
|
||||
mainRunLoop.run(until: Date() + 0.05)
|
||||
XCTAssertEqual(actualDate.timeIntervalSinceReferenceDate,
|
||||
now.timeIntervalSinceReferenceDate,
|
||||
accuracy: 0.1)
|
||||
}
|
||||
|
||||
func testScheduleActionOnceLater() {
|
||||
let mainRunLoop = RunLoop.main
|
||||
let now = Date()
|
||||
var actualDate = Date.distantPast
|
||||
let desiredDelay: TimeInterval = 0.6
|
||||
executeOnBackgroundThread {
|
||||
let scheduler = makeScheduler(mainRunLoop)
|
||||
scheduler
|
||||
.schedule(after: scheduler.now.advanced(by: .init(desiredDelay))) {
|
||||
// This is a bug in Combine! (FB7493579)
|
||||
// This should be XCTAssertTrue. When they fix it, this test will fail
|
||||
// and we'll know to fix our implementation.
|
||||
XCTAssertFalse(Thread.isMainThread)
|
||||
actualDate = Date()
|
||||
}
|
||||
RunLoop.current.run(until: Date() + 1)
|
||||
}
|
||||
|
||||
XCTAssertEqual(
|
||||
actualDate.timeIntervalSinceReferenceDate -
|
||||
now.timeIntervalSinceReferenceDate,
|
||||
desiredDelay,
|
||||
accuracy: desiredDelay / 3
|
||||
)
|
||||
}
|
||||
|
||||
func testScheduleRepeating() {
|
||||
let mainRunLoop = RunLoop.main
|
||||
|
||||
let expectation5ticks = expectation(description: "5 ticks")
|
||||
expectation5ticks.expectedFulfillmentCount = 10
|
||||
|
||||
let startDate = Date().timeIntervalSinceReferenceDate
|
||||
|
||||
let ticks = Atomic([TimeInterval]())
|
||||
|
||||
let desiredDelay: TimeInterval = 0.7
|
||||
let desiredInterval: TimeInterval = 0.3
|
||||
|
||||
let cancellable = executeOnBackgroundThread { () -> Cancellable in
|
||||
let scheduler = makeScheduler(mainRunLoop)
|
||||
return scheduler
|
||||
.schedule(after: scheduler.now.advanced(by: .init(desiredDelay)),
|
||||
interval: .init(desiredInterval)) {
|
||||
XCTAssertTrue(Thread.isMainThread)
|
||||
ticks.do {
|
||||
$0.append(Date().timeIntervalSinceReferenceDate)
|
||||
}
|
||||
expectation5ticks.fulfill()
|
||||
RunLoop.current.run(until: Date() + 0.001)
|
||||
}
|
||||
}
|
||||
|
||||
XCTAssertEqual(ticks.value.count, 0)
|
||||
mainRunLoop.run(until: Date() + 0.001)
|
||||
XCTAssertEqual(ticks.value.count, 0)
|
||||
|
||||
wait(for: [expectation5ticks], timeout: 5)
|
||||
|
||||
if ticks.value.isEmpty {
|
||||
XCTFail("The scheduler doesn't work")
|
||||
return
|
||||
}
|
||||
|
||||
let actualDelay = ticks.value[0] - startDate
|
||||
let actualIntervals = zip(ticks.value.dropFirst(), ticks.value.dropLast()).map(-)
|
||||
let averageInterval = actualIntervals.reduce(0, +) / Double(actualIntervals.count)
|
||||
|
||||
XCTAssertEqual(actualDelay,
|
||||
desiredDelay,
|
||||
accuracy: desiredDelay / 3,
|
||||
"""
|
||||
Actual delay (\(actualDelay)) deviates from desired delay \
|
||||
(\(desiredDelay)) too much
|
||||
""")
|
||||
|
||||
XCTAssertEqual(averageInterval,
|
||||
desiredInterval,
|
||||
accuracy: desiredInterval / 3,
|
||||
"""
|
||||
Actual average interval (\(averageInterval)) deviates from \
|
||||
desired interval (\(desiredInterval)) too much.
|
||||
|
||||
Actual intervals: \(actualIntervals)
|
||||
""")
|
||||
|
||||
cancellable.cancel()
|
||||
let numberOfTicksRightAfterCancellation = ticks.value.count
|
||||
mainRunLoop.run(until: Date() + 1)
|
||||
let numberOfTicksOneSecondAfterCancellation = ticks.value.count
|
||||
XCTAssertEqual(numberOfTicksRightAfterCancellation,
|
||||
numberOfTicksOneSecondAfterCancellation)
|
||||
}
|
||||
}
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST || !canImport(Combine)
|
||||
|
||||
private typealias Scheduler = RunLoop
|
||||
|
||||
private func makeScheduler(_ runLoop: RunLoop) -> RunLoop {
|
||||
return runLoop
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
private typealias Scheduler = RunLoop.OCombine
|
||||
|
||||
private func makeScheduler(_ runLoop: RunLoop) -> RunLoop.OCombine {
|
||||
return runLoop.ocombine
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private typealias Stride = Scheduler.SchedulerTimeType.Stride
|
||||
@@ -0,0 +1,724 @@
|
||||
//
|
||||
// URLSessionTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 13.12.2019.
|
||||
//
|
||||
|
||||
// swiftlint:disable multiline_arguments
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
#if canImport(FoundationNetworking)
|
||||
import FoundationNetworking
|
||||
#endif
|
||||
|
||||
// We can't test it on non-Darwin platforms because swift-corelibs-foundation
|
||||
// doesn't allow us to override some URLSession methods that we need.
|
||||
//
|
||||
// As soon as https://github.com/apple/swift-corelibs-foundation/pull/2587 makes it
|
||||
// into a release, we can enable these tests on non-Darwin platforms.
|
||||
//
|
||||
// The publisher itself though should work alright on those platforms.
|
||||
#if canImport(Darwin)
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
import OpenCombineFoundation
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class URLSessionTests: XCTestCase {
|
||||
|
||||
private typealias TrackingSubscriber =
|
||||
TrackingSubscriberBase<(data: Data, response: URLResponse), URLError>
|
||||
|
||||
private let testURL = URL(string: "https://github.com")!
|
||||
|
||||
private let testRequest = URLRequest(url: URL(string: "https://github.com")!,
|
||||
cachePolicy: .reloadIgnoringCacheData,
|
||||
timeoutInterval: 42)
|
||||
|
||||
private let testData = Data("test data".utf8)
|
||||
|
||||
private let testResponse = URLResponse(url: URL(string: "https://example.com")!,
|
||||
mimeType: "text/markdown",
|
||||
expectedContentLength: 300,
|
||||
textEncodingName: "utf-8")
|
||||
|
||||
private let testError = URLError(.cannotParseResponse, userInfo: ["a" : 1])
|
||||
|
||||
private let unknownError = URLError(.unknown)
|
||||
|
||||
func testDataTaskPublisherFromURL() {
|
||||
let publisher = makePublisher(TestURLSession(testDataTask: .init()), testURL)
|
||||
let expectedRequest = URLRequest(url: testURL)
|
||||
XCTAssertEqual(publisher.request, expectedRequest)
|
||||
}
|
||||
|
||||
func testDataTaskPublisherFromRequest() {
|
||||
let publisher = makePublisher(TestURLSession(testDataTask: .init()), testRequest)
|
||||
XCTAssertEqual(publisher.request, testRequest)
|
||||
}
|
||||
|
||||
func testReceiveNothing() {
|
||||
testReceiveResult(nil, nil, nil,
|
||||
expected: [.subscription("DataTaskPublisher"),
|
||||
.completion(.failure(unknownError))])
|
||||
}
|
||||
|
||||
func testReceiveOnlyData() {
|
||||
testReceiveResult(testData, nil, nil,
|
||||
expected: [.subscription("DataTaskPublisher"),
|
||||
.completion(.failure(unknownError))])
|
||||
}
|
||||
|
||||
func testReceiveDataAndResponse() {
|
||||
testReceiveResult(testData, testResponse, nil,
|
||||
expected: [.subscription("DataTaskPublisher"),
|
||||
.value((testData, testResponse)),
|
||||
.completion(.finished)])
|
||||
}
|
||||
|
||||
func testReceiveDataAndURLError() {
|
||||
testReceiveResult(testData, nil, testError,
|
||||
expected: [.subscription("DataTaskPublisher"),
|
||||
.completion(.failure(testError))])
|
||||
}
|
||||
|
||||
func testReceiveDataAndUnrelatedError() {
|
||||
testReceiveResult(testData, nil, TestingError.oops,
|
||||
expected: [.subscription("DataTaskPublisher"),
|
||||
.completion(.failure(unknownError))])
|
||||
}
|
||||
|
||||
func testReceiveOnlyResponse() {
|
||||
testReceiveResult(nil, testResponse, nil,
|
||||
expected: [.subscription("DataTaskPublisher"),
|
||||
.value((Data(), testResponse)),
|
||||
.completion(.finished)])
|
||||
}
|
||||
|
||||
func testReceiveResponseAndURLError() {
|
||||
testReceiveResult(nil, testResponse, testError,
|
||||
expected: [.subscription("DataTaskPublisher"),
|
||||
.completion(.failure(testError))])
|
||||
}
|
||||
|
||||
func testReceiveResponseAndUnrelatedError() {
|
||||
testReceiveResult(nil, testResponse, TestingError.oops,
|
||||
expected: [.subscription("DataTaskPublisher"),
|
||||
.completion(.failure(unknownError))])
|
||||
}
|
||||
|
||||
func testReceiveOnlyURLError() {
|
||||
testReceiveResult(nil, nil, testError,
|
||||
expected: [.subscription("DataTaskPublisher"),
|
||||
.completion(.failure(testError))])
|
||||
}
|
||||
|
||||
func testReceiveOnlyUnrelatedError() {
|
||||
testReceiveResult(nil, nil, TestingError.oops,
|
||||
expected: [.subscription("DataTaskPublisher"),
|
||||
.completion(.failure(unknownError))])
|
||||
}
|
||||
|
||||
func testRequesting() throws {
|
||||
let dataTask = TestURLSessionDataTask()
|
||||
let session = TestURLSession(testDataTask: dataTask)
|
||||
let publisher = makePublisher(session, testRequest)
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: { downstreamSubscription = $0 }
|
||||
)
|
||||
publisher.subscribe(tracking)
|
||||
|
||||
tracking.assertHistoryEqual([.subscription("DataTaskPublisher")],
|
||||
valueComparator: ==)
|
||||
XCTAssertEqual(dataTask.history, [])
|
||||
XCTAssertEqual(session.history, [])
|
||||
|
||||
session.completeDataTasks(testData, testResponse, nil)
|
||||
|
||||
try XCTUnwrap(downstreamSubscription).request(.max(2))
|
||||
try XCTUnwrap(downstreamSubscription).request(.max(1))
|
||||
|
||||
tracking.assertHistoryEqual([.subscription("DataTaskPublisher")],
|
||||
valueComparator: ==)
|
||||
XCTAssertEqual(dataTask.history, [.resume, .resume])
|
||||
XCTAssertEqual(session.history, [.dataTaskWithRequestAndCompletion(testRequest)])
|
||||
|
||||
try XCTUnwrap(downstreamSubscription).cancel()
|
||||
|
||||
tracking.assertHistoryEqual([.subscription("DataTaskPublisher")],
|
||||
valueComparator: ==)
|
||||
XCTAssertEqual(dataTask.history, [.resume, .resume, .cancel])
|
||||
XCTAssertEqual(session.history, [.dataTaskWithRequestAndCompletion(testRequest)])
|
||||
|
||||
session.completeDataTasks(testData, testResponse, nil)
|
||||
|
||||
tracking.assertHistoryEqual([.subscription("DataTaskPublisher")],
|
||||
valueComparator: ==)
|
||||
XCTAssertEqual(dataTask.history, [.resume, .resume, .cancel])
|
||||
XCTAssertEqual(session.history, [.dataTaskWithRequestAndCompletion(testRequest)])
|
||||
}
|
||||
|
||||
func testCancelAlreadyCancelled() throws {
|
||||
let dataTask = TestURLSessionDataTask()
|
||||
let session = TestURLSession(testDataTask: dataTask)
|
||||
let publisher = makePublisher(session, testRequest)
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: { downstreamSubscription = $0 }
|
||||
)
|
||||
publisher.subscribe(tracking)
|
||||
|
||||
tracking.assertHistoryEqual([.subscription("DataTaskPublisher")],
|
||||
valueComparator: ==)
|
||||
XCTAssertEqual(dataTask.history, [])
|
||||
XCTAssertEqual(session.history, [])
|
||||
|
||||
try XCTUnwrap(downstreamSubscription).request(.max(1))
|
||||
try XCTUnwrap(downstreamSubscription).cancel()
|
||||
try XCTUnwrap(downstreamSubscription).request(.max(1))
|
||||
try XCTUnwrap(downstreamSubscription).cancel()
|
||||
|
||||
tracking.assertHistoryEqual([.subscription("DataTaskPublisher")],
|
||||
valueComparator: ==)
|
||||
XCTAssertEqual(dataTask.history, [.resume, .cancel])
|
||||
XCTAssertEqual(session.history, [.dataTaskWithRequestAndCompletion(testRequest)])
|
||||
}
|
||||
|
||||
func testCrashesOnZeroDemand() throws {
|
||||
let dataTask = TestURLSessionDataTask()
|
||||
let session = TestURLSession(testDataTask: dataTask)
|
||||
let publisher = makePublisher(session, testURL)
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: { downstreamSubscription = $0 }
|
||||
)
|
||||
publisher.subscribe(tracking)
|
||||
|
||||
try assertCrashes {
|
||||
try XCTUnwrap(downstreamSubscription).request(.none)
|
||||
}
|
||||
}
|
||||
|
||||
func testURLSessionSubscriptionReflection() throws {
|
||||
let dataTask = TestURLSessionDataTask()
|
||||
let session = TestURLSession(testDataTask: dataTask)
|
||||
let publisher = makePublisher(session, testURL)
|
||||
try testSubscriptionReflection(
|
||||
description: "DataTaskPublisher",
|
||||
customMirror: expectedChildren(
|
||||
("task", "nil"),
|
||||
("downstream", .contains("TrackingSubscriberBase")),
|
||||
("parent", .matches(String(describing: Optional(publisher)))),
|
||||
("demand", "max(0)")
|
||||
),
|
||||
playgroundDescription: "DataTaskPublisher",
|
||||
sut: publisher
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Generic tests
|
||||
|
||||
private func testReceiveResult(_ data: Data?,
|
||||
_ response: URLResponse?,
|
||||
_ error: Error?,
|
||||
expected: [TrackingSubscriber.Event]) {
|
||||
let dataTask = TestURLSessionDataTask()
|
||||
let session = TestURLSession(testDataTask: dataTask)
|
||||
let publisher = makePublisher(session, testRequest)
|
||||
let tracking = TrackingSubscriber(receiveSubscription: { $0.request(.max(1)) })
|
||||
publisher.subscribe(tracking)
|
||||
|
||||
tracking.assertHistoryEqual([.subscription("DataTaskPublisher")],
|
||||
valueComparator: ==)
|
||||
XCTAssertEqual(dataTask.history, [.resume])
|
||||
XCTAssertEqual(session.history, [.dataTaskWithRequestAndCompletion(testRequest)])
|
||||
|
||||
session.completeDataTasks(data, response, error)
|
||||
session.completeDataTasks(data, response, error)
|
||||
session.completeDataTasks(data, response, error)
|
||||
|
||||
tracking.assertHistoryEqual(expected, valueComparator: ==)
|
||||
XCTAssertEqual(dataTask.history, [.resume])
|
||||
XCTAssertEqual(session.history, [.dataTaskWithRequestAndCompletion(testRequest)])
|
||||
}
|
||||
}
|
||||
|
||||
/// A simple mock URLSession that records its history and allows executing
|
||||
/// callbacks synchronously
|
||||
private class TestURLSession: URLSession {
|
||||
|
||||
enum Event: Equatable {
|
||||
case delegateQueue
|
||||
case delegate
|
||||
case configuration
|
||||
case getSessionDescription
|
||||
case setSessionDescription(String?)
|
||||
case finishTasksAndInvalidate
|
||||
case invalidateAndCancel
|
||||
case reset
|
||||
case flush
|
||||
case getTasksWithCompletionHandler
|
||||
case getAllTasks
|
||||
case dataTaskWithRequest(URLRequest)
|
||||
case dataTaskWithRequestAndCompletion(URLRequest)
|
||||
case dataTaskWithURL(URL)
|
||||
case dataTaskWithURLAndCompletion(URL)
|
||||
case uploadTaskWithRequestFromFile(URLRequest, URL)
|
||||
case uploadTaskWithRequestFromFileWithCompletion(URLRequest, URL)
|
||||
case uploadTaskWithRequestFromData(URLRequest, Data)
|
||||
case uploadTaskWithRequestFromDataWithCompletion(URLRequest, Data?)
|
||||
case uploadTaskWithStreamedRequest(URLRequest)
|
||||
case downloadTaskWithRequest(URLRequest)
|
||||
case downloadTaskWithRequestAndCompletion(URLRequest)
|
||||
case downloadTaskWithURL(URL)
|
||||
case downloadTaskWithURLAndCompletion(URL)
|
||||
case downloadTaskWithResumeData(Data)
|
||||
case downloadTaskWithResumeDataAndCompletion(Data)
|
||||
case streamTaskWithHostNameAndPort(String, Int)
|
||||
#if canImport(Darwin) && swift(>=5.1)
|
||||
case streamTaskWithService(NetService)
|
||||
case webSocketTaskWithURL(URL)
|
||||
case webSocketTaskWithURLAndProtocols(URL, [String])
|
||||
case webSocketTaskWithRequest(URLRequest)
|
||||
#endif // canImport(Darwin) && swift(>=5.1)
|
||||
}
|
||||
|
||||
private(set) var history = [Event]()
|
||||
|
||||
private(set) var dataTaskCompletionHandlers: [(Data?, URLResponse?, Error?) -> Void]
|
||||
|
||||
private let testDataTask: TestURLSessionDataTask
|
||||
|
||||
init(testDataTask: TestURLSessionDataTask) {
|
||||
self.testDataTask = testDataTask
|
||||
self.dataTaskCompletionHandlers = []
|
||||
}
|
||||
|
||||
// MARK: Testing
|
||||
|
||||
func completeDataTasks(_ data: Data?, _ response: URLResponse?, _ error: Error?) {
|
||||
for completionHandler in dataTaskCompletionHandlers {
|
||||
completionHandler(data, response, error)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Overrides
|
||||
|
||||
override class var shared: URLSession { fatalError("shared session is unavailable") }
|
||||
|
||||
override var delegateQueue: OperationQueue {
|
||||
history.append(.delegateQueue)
|
||||
return super.delegateQueue
|
||||
}
|
||||
|
||||
override var delegate: URLSessionDelegate? {
|
||||
history.append(.delegate)
|
||||
return super.delegate
|
||||
}
|
||||
|
||||
override var configuration: URLSessionConfiguration {
|
||||
history.append(.configuration)
|
||||
return super.configuration
|
||||
}
|
||||
|
||||
override var sessionDescription: String? {
|
||||
get {
|
||||
history.append(.getSessionDescription)
|
||||
return super.sessionDescription
|
||||
}
|
||||
set {
|
||||
history.append(.setSessionDescription(newValue))
|
||||
super.sessionDescription = newValue
|
||||
}
|
||||
}
|
||||
|
||||
override func finishTasksAndInvalidate() {
|
||||
history.append(.finishTasksAndInvalidate)
|
||||
super.finishTasksAndInvalidate()
|
||||
}
|
||||
|
||||
override func invalidateAndCancel() {
|
||||
history.append(.invalidateAndCancel)
|
||||
super.invalidateAndCancel()
|
||||
}
|
||||
|
||||
override func reset(completionHandler: @escaping () -> Void) {
|
||||
history.append(.reset)
|
||||
super.reset(completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
override func flush(completionHandler: @escaping () -> Void) {
|
||||
history.append(.flush)
|
||||
super.flush(completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
override func getTasksWithCompletionHandler(
|
||||
_ completionHandler: @escaping ([URLSessionDataTask],
|
||||
[URLSessionUploadTask],
|
||||
[URLSessionDownloadTask]) -> Void
|
||||
) {
|
||||
history.append(.getTasksWithCompletionHandler)
|
||||
super.getTasksWithCompletionHandler(completionHandler)
|
||||
}
|
||||
|
||||
@available(macOS 10.11, iOS 9.0, *)
|
||||
override func getAllTasks(completionHandler: @escaping ([URLSessionTask]) -> Void) {
|
||||
history.append(.getAllTasks)
|
||||
super.getAllTasks(completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
override func dataTask(with request: URLRequest) -> URLSessionDataTask {
|
||||
history.append(.dataTaskWithRequest(request))
|
||||
return testDataTask
|
||||
}
|
||||
|
||||
override func dataTask(with url: URL) -> URLSessionDataTask {
|
||||
history.append(.dataTaskWithURL(url))
|
||||
return testDataTask
|
||||
}
|
||||
|
||||
override func dataTask(
|
||||
with url: URL,
|
||||
completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void
|
||||
) -> URLSessionDataTask {
|
||||
history.append(.dataTaskWithURLAndCompletion(url))
|
||||
dataTaskCompletionHandlers.append(completionHandler)
|
||||
return testDataTask
|
||||
}
|
||||
|
||||
override func dataTask(
|
||||
with request: URLRequest,
|
||||
completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void
|
||||
) -> URLSessionDataTask {
|
||||
history.append(.dataTaskWithRequestAndCompletion(request))
|
||||
dataTaskCompletionHandlers.append(completionHandler)
|
||||
return testDataTask
|
||||
}
|
||||
|
||||
override func uploadTask(with request: URLRequest,
|
||||
fromFile fileURL: URL) -> URLSessionUploadTask {
|
||||
history.append(.uploadTaskWithRequestFromFile(request, fileURL))
|
||||
return super.uploadTask(with: request, fromFile: fileURL)
|
||||
}
|
||||
|
||||
override func uploadTask(with request: URLRequest,
|
||||
from bodyData: Data) -> URLSessionUploadTask {
|
||||
history.append(.uploadTaskWithRequestFromData(request, bodyData))
|
||||
return super.uploadTask(with: request, from: bodyData)
|
||||
}
|
||||
|
||||
override func uploadTask(
|
||||
withStreamedRequest request: URLRequest
|
||||
) -> URLSessionUploadTask {
|
||||
history.append(.uploadTaskWithStreamedRequest(request))
|
||||
return super.uploadTask(withStreamedRequest: request)
|
||||
}
|
||||
|
||||
override func uploadTask(
|
||||
with request: URLRequest,
|
||||
fromFile fileURL: URL,
|
||||
completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void
|
||||
) -> URLSessionUploadTask {
|
||||
history.append(.uploadTaskWithRequestFromFileWithCompletion(request, fileURL))
|
||||
return super.uploadTask(with: request,
|
||||
fromFile: fileURL,
|
||||
completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
override func uploadTask(
|
||||
with request: URLRequest,
|
||||
from bodyData: Data?,
|
||||
completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void
|
||||
) -> URLSessionUploadTask {
|
||||
history.append(.uploadTaskWithRequestFromDataWithCompletion(request, bodyData))
|
||||
return super.uploadTask(with: request,
|
||||
from: bodyData,
|
||||
completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
override func downloadTask(with request: URLRequest) -> URLSessionDownloadTask {
|
||||
history.append(.downloadTaskWithRequest(request))
|
||||
return super.downloadTask(with: request)
|
||||
}
|
||||
|
||||
override func downloadTask(with url: URL) -> URLSessionDownloadTask {
|
||||
history.append(.downloadTaskWithURL(url))
|
||||
return super.downloadTask(with: url)
|
||||
}
|
||||
|
||||
override func downloadTask(
|
||||
with request: URLRequest,
|
||||
completionHandler: @escaping (URL?, URLResponse?, Error?) -> Void
|
||||
) -> URLSessionDownloadTask {
|
||||
history.append(.downloadTaskWithRequestAndCompletion(request))
|
||||
return super.downloadTask(with: request, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
override func downloadTask(
|
||||
with url: URL,
|
||||
completionHandler: @escaping (URL?, URLResponse?, Error?) -> Void
|
||||
) -> URLSessionDownloadTask {
|
||||
history.append(.downloadTaskWithURLAndCompletion(url))
|
||||
return super.downloadTask(with: url, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
override func downloadTask(
|
||||
withResumeData resumeData: Data,
|
||||
completionHandler: @escaping (URL?, URLResponse?, Error?) -> Void
|
||||
) -> URLSessionDownloadTask {
|
||||
history.append(.downloadTaskWithResumeDataAndCompletion(resumeData))
|
||||
return super.downloadTask(withResumeData: resumeData,
|
||||
completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
override func downloadTask(
|
||||
withResumeData resumeData: Data
|
||||
) -> URLSessionDownloadTask {
|
||||
history.append(.downloadTaskWithResumeData(resumeData))
|
||||
return super.downloadTask(withResumeData: resumeData)
|
||||
}
|
||||
|
||||
@available(macOS 10.11, iOS 9.0, *)
|
||||
override func streamTask(withHostName hostname: String,
|
||||
port: Int) -> URLSessionStreamTask {
|
||||
history.append(.streamTaskWithHostNameAndPort(hostname, port))
|
||||
return super.streamTask(withHostName: hostname, port: port)
|
||||
}
|
||||
|
||||
#if canImport(Darwin) && swift(>=5.1)
|
||||
@available(macOS 10.11, iOS 9.0, *)
|
||||
override func streamTask(with service: NetService) -> URLSessionStreamTask {
|
||||
history.append(.streamTaskWithService(service))
|
||||
return super.streamTask(with: service)
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
override func webSocketTask(with url: URL) -> URLSessionWebSocketTask {
|
||||
history.append(.webSocketTaskWithURL(url))
|
||||
return super.webSocketTask(with: url)
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
override func webSocketTask(with url: URL,
|
||||
protocols: [String]) -> URLSessionWebSocketTask {
|
||||
history.append(.webSocketTaskWithURLAndProtocols(url, protocols))
|
||||
return super.webSocketTask(with: url, protocols: protocols)
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
override func webSocketTask(with request: URLRequest) -> URLSessionWebSocketTask {
|
||||
history.append(.webSocketTaskWithRequest(request))
|
||||
return super.webSocketTask(with: request)
|
||||
}
|
||||
#endif // canImport(Darwin) && swift(>=5.1)
|
||||
}
|
||||
|
||||
private final class TestURLSessionDataTask: URLSessionDataTask {
|
||||
|
||||
enum Event: Equatable {
|
||||
case taskIdentifier
|
||||
case originalRequest
|
||||
case currentRequest
|
||||
case response
|
||||
case progress
|
||||
case getEarliestBeginDate
|
||||
case setEarliestBeginDate(Date?)
|
||||
case getCountOfBytesClientExpectsToSend
|
||||
case setCountOfBytesClientExpectsToSend(Int64)
|
||||
case getCountOfBytesClientExpectsToReceive
|
||||
case setCountOfBytesClientExpectsToReceive(Int64)
|
||||
case countOfBytesReceived
|
||||
case countOfBytesSent
|
||||
case countOfBytesExpectedToSend
|
||||
case countOfBytesExpectedToReceive
|
||||
case getTaskDescription
|
||||
case setTaskDescription(String?)
|
||||
case cancel
|
||||
case state
|
||||
case error
|
||||
case suspend
|
||||
case resume
|
||||
case getPriority
|
||||
case setPriority(Float)
|
||||
}
|
||||
|
||||
private(set) var history = [Event]()
|
||||
|
||||
override init() {}
|
||||
|
||||
override var taskIdentifier: Int {
|
||||
history.append(.taskIdentifier)
|
||||
return super.taskIdentifier
|
||||
}
|
||||
|
||||
override var originalRequest: URLRequest? {
|
||||
history.append(.originalRequest)
|
||||
return super.originalRequest
|
||||
}
|
||||
|
||||
override var currentRequest: URLRequest? {
|
||||
history.append(.currentRequest)
|
||||
return super.currentRequest
|
||||
}
|
||||
|
||||
override var response: URLResponse? {
|
||||
history.append(.response)
|
||||
return super.response
|
||||
}
|
||||
|
||||
@available(macOS 10.13, iOS 11.0, *)
|
||||
override var progress: Progress {
|
||||
history.append(.progress)
|
||||
return super.progress
|
||||
}
|
||||
|
||||
@available(macOS 10.13, iOS 11.0, *)
|
||||
override var earliestBeginDate: Date? {
|
||||
get {
|
||||
history.append(.getEarliestBeginDate)
|
||||
#if canImport(Darwin)
|
||||
return super.earliestBeginDate
|
||||
#else
|
||||
return nil // Deprecated in swift-corerlibs-foundation
|
||||
#endif
|
||||
}
|
||||
set {
|
||||
history.append(.setEarliestBeginDate(newValue))
|
||||
#if canImport(Darwin)
|
||||
super.earliestBeginDate = newValue
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 10.13, iOS 11.0, *)
|
||||
override var countOfBytesClientExpectsToSend: Int64 {
|
||||
get {
|
||||
history.append(.getCountOfBytesClientExpectsToSend)
|
||||
return super.countOfBytesClientExpectsToSend
|
||||
}
|
||||
set {
|
||||
history.append(.setCountOfBytesClientExpectsToSend(newValue))
|
||||
super.countOfBytesClientExpectsToSend = newValue
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 10.13, iOS 11.0, *)
|
||||
override var countOfBytesClientExpectsToReceive: Int64 {
|
||||
get {
|
||||
history.append(.getCountOfBytesClientExpectsToReceive)
|
||||
return super.countOfBytesClientExpectsToReceive
|
||||
}
|
||||
set {
|
||||
history.append(.setCountOfBytesClientExpectsToReceive(newValue))
|
||||
super.countOfBytesClientExpectsToReceive = newValue
|
||||
}
|
||||
}
|
||||
|
||||
override var countOfBytesReceived: Int64 {
|
||||
history.append(.countOfBytesReceived)
|
||||
return super.countOfBytesReceived
|
||||
}
|
||||
|
||||
override var countOfBytesSent: Int64 {
|
||||
history.append(.countOfBytesSent)
|
||||
return super.countOfBytesSent
|
||||
}
|
||||
|
||||
override var countOfBytesExpectedToSend: Int64 {
|
||||
history.append(.countOfBytesExpectedToSend)
|
||||
return super.countOfBytesExpectedToSend
|
||||
}
|
||||
|
||||
override var countOfBytesExpectedToReceive: Int64 {
|
||||
history.append(.countOfBytesExpectedToReceive)
|
||||
return super.countOfBytesExpectedToReceive
|
||||
}
|
||||
|
||||
override var taskDescription: String? {
|
||||
get {
|
||||
history.append(.getTaskDescription)
|
||||
return super.taskDescription
|
||||
}
|
||||
set {
|
||||
history.append(.setTaskDescription(newValue))
|
||||
super.taskDescription = newValue
|
||||
}
|
||||
}
|
||||
|
||||
override func cancel() {
|
||||
history.append(.cancel)
|
||||
}
|
||||
|
||||
override var state: URLSessionTask.State {
|
||||
history.append(.state)
|
||||
return super.state
|
||||
}
|
||||
|
||||
override var error: Error? {
|
||||
history.append(.error)
|
||||
return super.error
|
||||
}
|
||||
|
||||
override func suspend() {
|
||||
history.append(.suspend)
|
||||
}
|
||||
|
||||
override func resume() {
|
||||
history.append(.resume)
|
||||
}
|
||||
|
||||
override var priority: Float {
|
||||
get {
|
||||
history.append(.getPriority)
|
||||
return super.priority
|
||||
}
|
||||
set {
|
||||
history.append(.setPriority(newValue))
|
||||
super.priority = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension URLError: EquatableError {}
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST || !canImport(Combine)
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private func makePublisher(
|
||||
_ session: URLSession,
|
||||
_ url: URL
|
||||
) -> URLSession.DataTaskPublisher {
|
||||
return session.dataTaskPublisher(for: url)
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private func makePublisher(
|
||||
_ session: URLSession,
|
||||
_ request: URLRequest
|
||||
) -> URLSession.DataTaskPublisher {
|
||||
return session.dataTaskPublisher(for: request)
|
||||
}
|
||||
#else
|
||||
private func makePublisher(
|
||||
_ session: URLSession,
|
||||
_ url: URL
|
||||
) -> URLSession.OCombine.DataTaskPublisher {
|
||||
return session.ocombine.dataTaskPublisher(for: url)
|
||||
}
|
||||
|
||||
private func makePublisher(
|
||||
_ session: URLSession,
|
||||
_ request: URLRequest
|
||||
) -> URLSession.OCombine.DataTaskPublisher {
|
||||
return session.ocombine.dataTaskPublisher(for: request)
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif // canImport(Darwin)
|
||||
@@ -164,13 +164,6 @@ extension XCTest {
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.cancelled, .cancelled])
|
||||
|
||||
let thirdSubscription = CustomSubscription()
|
||||
|
||||
try XCTUnwrap(helper.publisher.subscriber)
|
||||
.receive(subscription: thirdSubscription)
|
||||
|
||||
XCTAssertEqual(thirdSubscription.history, [.cancelled])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,4 +220,8 @@ func unreachable<T>(_: T) -> Never {
|
||||
fatalError("unreachable")
|
||||
}
|
||||
|
||||
func unreachable() -> Never {
|
||||
fatalError("unreachable")
|
||||
}
|
||||
|
||||
// swiftlint:enable generic_type_name
|
||||
|
||||
@@ -34,26 +34,35 @@ import OpenCombine
|
||||
typealias CustomPublisher = CustomPublisherBase<Int, TestingError>
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
class CustomPublisherBase<Output, Failure: Error>: Publisher {
|
||||
class CustomPublisherBase<Output, Failure: Error>: Publisher, Cancellable {
|
||||
|
||||
private(set) var subscriber: AnySubscriber<Output, Failure>?
|
||||
private(set) var erasedSubscriber: Any?
|
||||
private let subscription: Subscription?
|
||||
|
||||
var onSubscribe: ((AnySubscriber<Output, Failure>) -> Void)?
|
||||
var willSubscribe: ((AnySubscriber<Output, Failure>) -> Void)?
|
||||
|
||||
var didSubscribe: ((AnySubscriber<Output, Failure>) -> Void)?
|
||||
|
||||
var onDeinit: (() -> Void)?
|
||||
|
||||
required init(subscription: Subscription?) {
|
||||
self.subscription = subscription
|
||||
}
|
||||
|
||||
deinit {
|
||||
onDeinit?()
|
||||
}
|
||||
|
||||
func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Failure == Downstream.Failure, Output == Downstream.Input
|
||||
{
|
||||
let anySubscriber = AnySubscriber(subscriber)
|
||||
self.subscriber = anySubscriber
|
||||
onSubscribe?(anySubscriber)
|
||||
willSubscribe?(anySubscriber)
|
||||
erasedSubscriber = subscriber
|
||||
subscription.map(subscriber.receive(subscription:))
|
||||
didSubscribe?(anySubscriber)
|
||||
}
|
||||
|
||||
func send(subscription: CustomSubscription) {
|
||||
@@ -67,13 +76,18 @@ class CustomPublisherBase<Output, Failure: Error>: Publisher {
|
||||
func send(completion: Subscribers.Completion<Failure>) {
|
||||
subscriber?.receive(completion: completion)
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
subscriber = nil
|
||||
erasedSubscriber = nil
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
typealias CustomConnectablePublisher = CustomConnectablePublisherBase<Int, TestingError>
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class CustomConnectablePublisherBase<Output: Equatable, Failure: Error>
|
||||
final class CustomConnectablePublisherBase<Output, Failure: Error>
|
||||
: CustomPublisherBase<Output, Failure>,
|
||||
ConnectablePublisher
|
||||
{
|
||||
|
||||
@@ -38,11 +38,18 @@ final class CustomSubscription: Subscription, CustomStringConvertible {
|
||||
|
||||
var onRequest: ((Subscribers.Demand) -> Void)?
|
||||
var onCancel: (() -> Void)?
|
||||
var onDeinit: (() -> Void)?
|
||||
|
||||
init(onRequest: ((Subscribers.Demand) -> Void)? = nil,
|
||||
onCancel: (() -> Void)? = nil) {
|
||||
onCancel: (() -> Void)? = nil,
|
||||
onDeinit: (() -> Void)? = nil) {
|
||||
self.onRequest = onRequest
|
||||
self.onCancel = onCancel
|
||||
self.onDeinit = onDeinit
|
||||
}
|
||||
|
||||
deinit {
|
||||
onDeinit?()
|
||||
}
|
||||
|
||||
var lastRequested: Subscribers.Demand? {
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
//
|
||||
// ExecuteOnBackgroundThread.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 04.02.2020.
|
||||
//
|
||||
|
||||
#if canImport(Darwin)
|
||||
import Darwin
|
||||
#elseif canImport(Glibc)
|
||||
import Glibc
|
||||
#else
|
||||
#error("How to do threads on this platform?")
|
||||
#endif
|
||||
|
||||
#if canImport(Darwin)
|
||||
private typealias ThreadPtr = UnsafeMutablePointer<pthread_t?>
|
||||
#else
|
||||
private typealias ThreadPtr = UnsafeMutablePointer<pthread_t>
|
||||
#endif
|
||||
|
||||
func executeOnBackgroundThread<ResultType>(
|
||||
_ body: () -> ResultType
|
||||
) -> ResultType {
|
||||
return withoutActuallyEscaping(body) { body in
|
||||
|
||||
// We need this because @convention(c) closures can't capture generic params.
|
||||
var typeErasedBody: () -> UnsafeMutableRawPointer = {
|
||||
let resultPtr = UnsafeMutablePointer<ResultType>.allocate(capacity: 1)
|
||||
resultPtr.initialize(to: body())
|
||||
return UnsafeMutableRawPointer(resultPtr)
|
||||
}
|
||||
|
||||
return withUnsafeMutablePointer(to: &typeErasedBody) { typeErasedBody in
|
||||
let _backgroundThread = ThreadPtr.allocate(capacity: 1)
|
||||
|
||||
defer { _backgroundThread.deallocate() }
|
||||
|
||||
var status: Int32 = 0
|
||||
|
||||
// We could use Foundation's Thread, but it doesn't work on Linux for some
|
||||
// reason.
|
||||
status = pthread_create(
|
||||
_backgroundThread,
|
||||
nil,
|
||||
{ context in
|
||||
#if canImport(Darwin)
|
||||
let context = context
|
||||
#else
|
||||
let context = context!
|
||||
#endif
|
||||
return context
|
||||
.assumingMemoryBound(to: (() -> UnsafeMutableRawPointer).self)
|
||||
.pointee()
|
||||
},
|
||||
typeErasedBody
|
||||
)
|
||||
|
||||
guard status == 0 else {
|
||||
preconditionFailure("Could not create a background thread")
|
||||
}
|
||||
|
||||
#if canImport(Darwin)
|
||||
guard let backgroundThread = _backgroundThread.pointee else {
|
||||
preconditionFailure("Could not create a background thread")
|
||||
}
|
||||
#else
|
||||
let backgroundThread = _backgroundThread.pointee
|
||||
#endif
|
||||
|
||||
var _resultPtr: UnsafeMutableRawPointer?
|
||||
status = pthread_join(backgroundThread, &_resultPtr)
|
||||
|
||||
guard status == 0, let resultPtr = _resultPtr else {
|
||||
preconditionFailure("Could not join threads")
|
||||
}
|
||||
|
||||
defer { resultPtr.deallocate() }
|
||||
|
||||
return resultPtr.assumingMemoryBound(to: ResultType.self).move()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
//
|
||||
// FairPriorityQueue.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 02.12.2019.
|
||||
//
|
||||
|
||||
/// A priproty queue based on binary min-heap.
|
||||
/// If two elements with the same priority are added, the element that was added
|
||||
/// earlier has will have "better" priority (i. e. it will be also extracted earlier).
|
||||
struct FairPriorityQueue<Priority: Comparable, Element> {
|
||||
|
||||
private var storage: [((Priority, UInt), Element)] = []
|
||||
private var next: UInt = 0
|
||||
|
||||
init() {}
|
||||
|
||||
mutating func insert(_ element: Element, priority: Priority) {
|
||||
storage.append(((priority, next), element))
|
||||
next += 1
|
||||
var newElementIndex = storage.endIndex - 1
|
||||
while let parent = self.parent(of: newElementIndex),
|
||||
storage[parent].0 > storage[newElementIndex].0 {
|
||||
storage.swapAt(newElementIndex, parent)
|
||||
newElementIndex = parent
|
||||
}
|
||||
}
|
||||
|
||||
func min() -> Element? {
|
||||
return storage.first?.1
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
mutating func extractMin() -> (Priority, Element)? {
|
||||
guard let max = storage.first else { return nil }
|
||||
storage[0] = storage[storage.endIndex - 1]
|
||||
storage.removeLast()
|
||||
minHeapify(0)
|
||||
return (max.0.0, max.1)
|
||||
}
|
||||
|
||||
var count: Int {
|
||||
return storage.count
|
||||
}
|
||||
|
||||
var isEmpty: Bool {
|
||||
return storage.isEmpty
|
||||
}
|
||||
|
||||
private func leftChild(of index: Int) -> Int? {
|
||||
assert(index >= 0)
|
||||
let childIndex = 2 * index + 1
|
||||
return childIndex < storage.endIndex ? childIndex : nil
|
||||
}
|
||||
|
||||
private func rightChild(of index: Int) -> Int? {
|
||||
assert(index >= 0)
|
||||
let childIndex = 2 * index + 2
|
||||
return childIndex < storage.endIndex ? childIndex : nil
|
||||
}
|
||||
|
||||
private func parent(of index: Int) -> Int? {
|
||||
assert(index >= 0)
|
||||
if index == 0 { return nil }
|
||||
return (index - 1) / 2
|
||||
}
|
||||
|
||||
private mutating func minHeapify(_ root: Int) {
|
||||
var root = root
|
||||
var largest = root
|
||||
while true {
|
||||
assert(largest == root)
|
||||
if let left = leftChild(of: root), storage[root].0 > storage[left].0 {
|
||||
largest = left
|
||||
}
|
||||
if let right = rightChild(of: root), storage[largest].0 > storage[right].0 {
|
||||
largest = right
|
||||
}
|
||||
if largest == root {
|
||||
break
|
||||
}
|
||||
storage.swapAt(root, largest)
|
||||
root = largest
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension FairPriorityQueue: Sequence {
|
||||
struct Iterator: IteratorProtocol {
|
||||
private var queue: FairPriorityQueue
|
||||
|
||||
fileprivate init(_ queue: FairPriorityQueue) {
|
||||
self.queue = queue
|
||||
}
|
||||
|
||||
mutating func next() -> (Priority, Element)? {
|
||||
return queue.extractMin()
|
||||
}
|
||||
}
|
||||
|
||||
func makeIterator() -> Iterator {
|
||||
return Iterator(self)
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ func testLifecycle<UpstreamOutput, Operator: Publisher>(
|
||||
line: UInt = #line,
|
||||
sendValue valueToBeSent: UpstreamOutput,
|
||||
cancellingSubscriptionReleasesSubscriber: Bool,
|
||||
finishingIsPassedThrough: Bool = true,
|
||||
_ makeOperator: (PassthroughSubject<UpstreamOutput, TestingError>) -> Operator
|
||||
) throws {
|
||||
var deinitCounter = 0
|
||||
@@ -101,7 +102,11 @@ func testLifecycle<UpstreamOutput, Operator: Publisher>(
|
||||
file: file,
|
||||
line: line)
|
||||
|
||||
try XCTUnwrap(subscription, file: file, line: line).cancel()
|
||||
try XCTUnwrap(subscription,
|
||||
"Lifecycle test #3: subscription should be saved",
|
||||
file: file,
|
||||
line: line)
|
||||
.cancel()
|
||||
|
||||
if cancellingSubscriptionReleasesSubscriber {
|
||||
XCTAssertEqual(deinitCounter,
|
||||
@@ -134,8 +139,15 @@ func testLifecycle<UpstreamOutput, Operator: Publisher>(
|
||||
passthrough.send(completion: .finished)
|
||||
}
|
||||
|
||||
XCTAssertTrue(subscriberDestroyed,
|
||||
"Lifecycle test #4: deinit should be called",
|
||||
file: file,
|
||||
line: line)
|
||||
if finishingIsPassedThrough {
|
||||
XCTAssertTrue(subscriberDestroyed,
|
||||
"Lifecycle test #4: deinit should be called",
|
||||
file: file,
|
||||
line: line)
|
||||
} else {
|
||||
XCTAssertFalse(subscriberDestroyed,
|
||||
"Lifecycle test #4: deinit should not be called",
|
||||
file: file,
|
||||
line: line)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,10 +15,10 @@ import OpenCombine
|
||||
|
||||
let childrenIsEmpty: (Mirror) -> Bool = { $0.children.isEmpty }
|
||||
|
||||
enum ExpectedMirrorChildValue: Equatable, ExpressibleByStringLiteral {
|
||||
enum ExpectedMirrorChildValue: ExpressibleByStringLiteral {
|
||||
case anything
|
||||
case matches(String)
|
||||
case contains(String)
|
||||
case matches(@autoclosure () -> String)
|
||||
case contains(@autoclosure () -> String)
|
||||
|
||||
typealias StringLiteralType = String
|
||||
|
||||
@@ -49,10 +49,11 @@ func expectedChildren(_ expectedChildren: (String?, ExpectedMirrorChildValue)...
|
||||
case (_, .anything):
|
||||
continue
|
||||
case let (lhs, .matches(rhs)):
|
||||
XCTAssertEqual(lhs, rhs, file: file, line: line)
|
||||
XCTAssertEqual(lhs, rhs(), file: file, line: line)
|
||||
case let (lhs, .contains(rhs)):
|
||||
XCTAssert(lhs.contains(rhs),
|
||||
"\"\(lhs)\" doesn't contain substring \"\(rhs)\"",
|
||||
let evaluatedRHS = rhs()
|
||||
XCTAssert(lhs.contains(evaluatedRHS),
|
||||
"\"\(lhs)\" doesn't contain substring \"\(evaluatedRHS)\"",
|
||||
file: file,
|
||||
line: line)
|
||||
}
|
||||
@@ -67,7 +68,7 @@ func reduceLikeOperatorMirror(file: StaticString = #file,
|
||||
("downstream", .contains("TrackingSubscriberBase")),
|
||||
("result", .anything),
|
||||
("initial", .anything),
|
||||
("status", .contains("awaitingSubscription")),
|
||||
("status", .anything),
|
||||
file: file,
|
||||
line: line
|
||||
)
|
||||
@@ -79,9 +80,10 @@ internal func testReflection<Output, Failure: Error, Operator: Publisher>(
|
||||
line: UInt = #line,
|
||||
parentInput: Output.Type,
|
||||
parentFailure: Failure.Type,
|
||||
description expectedDescription: String,
|
||||
description expectedDescription: String?,
|
||||
customMirror customMirrorPredicate: ((Mirror) -> Bool)?,
|
||||
playgroundDescription: String,
|
||||
playgroundDescription: String?,
|
||||
subscriberIsAlsoSubscription: Bool = true,
|
||||
_ makeOperator: (CustomConnectablePublisherBase<Output, Failure>) -> Operator
|
||||
) throws {
|
||||
let publisher = CustomConnectablePublisherBase<Output, Failure>(subscription: nil)
|
||||
@@ -97,16 +99,18 @@ internal func testReflection<Output, Failure: Error, Operator: Publisher>(
|
||||
file: file,
|
||||
line: line)
|
||||
|
||||
let customMirror =
|
||||
try XCTUnwrap((erasedSubscriber as? CustomReflectable)?.customMirror,
|
||||
file: file,
|
||||
line: line)
|
||||
|
||||
if let customMirrorPredicate = customMirrorPredicate {
|
||||
let customMirror =
|
||||
try XCTUnwrap((erasedSubscriber as? CustomReflectable)?.customMirror,
|
||||
file: file,
|
||||
line: line)
|
||||
XCTAssert(customMirrorPredicate(customMirror),
|
||||
"customMirror doesn't satisfy the predicate",
|
||||
file: file,
|
||||
line: line)
|
||||
} else {
|
||||
XCTAssertFalse(erasedSubscriber is CustomReflectable,
|
||||
"subscriber shouldn't conform to CustomReflectable")
|
||||
}
|
||||
|
||||
XCTAssertFalse(erasedSubscriber is CustomDebugStringConvertible,
|
||||
@@ -121,6 +125,43 @@ internal func testReflection<Output, Failure: Error, Operator: Publisher>(
|
||||
file: file,
|
||||
line: line
|
||||
)
|
||||
|
||||
if subscriberIsAlsoSubscription {
|
||||
publisher.send(subscription: CustomSubscription())
|
||||
let subscription = try XCTUnwrap(tracking.subscriptions.first?.underlying)
|
||||
|
||||
XCTAssertEqual((subscription as? CustomStringConvertible)?.description,
|
||||
expectedDescription,
|
||||
file: file,
|
||||
line: line)
|
||||
|
||||
if let customMirrorPredicate = customMirrorPredicate {
|
||||
let customMirror =
|
||||
try XCTUnwrap((subscription as? CustomReflectable)?.customMirror,
|
||||
file: file,
|
||||
line: line)
|
||||
XCTAssert(customMirrorPredicate(customMirror),
|
||||
"customMirror doesn't satisfy the predicate",
|
||||
file: file,
|
||||
line: line)
|
||||
} else {
|
||||
XCTAssertFalse(subscription is CustomReflectable,
|
||||
"subscription shouldn't conform to CustomReflectable")
|
||||
}
|
||||
|
||||
XCTAssertFalse(subscription is CustomDebugStringConvertible,
|
||||
"subscription shouldn't conform to CustomDebugStringConvertible",
|
||||
file: file,
|
||||
line: line)
|
||||
|
||||
XCTAssertEqual(
|
||||
((subscription as? CustomPlaygroundDisplayConvertible)?
|
||||
.playgroundDescription as? String),
|
||||
playgroundDescription,
|
||||
file: file,
|
||||
line: line
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
@@ -131,7 +172,7 @@ internal func testSubscriptionReflection<Sut: Publisher>(
|
||||
customMirror customMirrorPredicate: ((Mirror) -> Bool)?,
|
||||
playgroundDescription: String,
|
||||
sut: Sut
|
||||
) throws where Sut.Output: Equatable {
|
||||
) throws {
|
||||
let tracking = TrackingSubscriberBase<Sut.Output, Sut.Failure>()
|
||||
sut.subscribe(tracking)
|
||||
|
||||
@@ -159,7 +200,7 @@ internal func testSubscriptionReflection<Sut: Publisher>(
|
||||
}
|
||||
|
||||
XCTAssertFalse(subscription is CustomDebugStringConvertible,
|
||||
"subscriber shouldn't conform to CustomDebugStringConvertible",
|
||||
"Subscription shouldn't conform to CustomDebugStringConvertible",
|
||||
file: file,
|
||||
line: line)
|
||||
|
||||
|
||||
@@ -40,6 +40,20 @@ extension TestingError: ExpressibleByStringLiteral {
|
||||
}
|
||||
}
|
||||
|
||||
protocol EquatableError: Error {
|
||||
func isEqual(_ other: EquatableError) -> Bool
|
||||
}
|
||||
|
||||
extension EquatableError where Self: Equatable {
|
||||
func isEqual(_ other: EquatableError) -> Bool {
|
||||
return self == (other as? Self)
|
||||
}
|
||||
}
|
||||
|
||||
extension TestingError: EquatableError {}
|
||||
|
||||
extension NSError: EquatableError {}
|
||||
|
||||
func assertThrowsError<Result>(_ expression: @autoclosure () throws -> Result,
|
||||
_ expected: TestingError,
|
||||
_ message: @autoclosure () -> String = "",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
// Created by Sergej Jaskiewicz on 11.06.2019.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
@@ -177,6 +178,10 @@ final class TrackingSubscriberBase<Value, Failure: Error>
|
||||
for subscription in subscriptions {
|
||||
subscription.cancel()
|
||||
}
|
||||
clearHistory()
|
||||
}
|
||||
|
||||
func clearHistory() {
|
||||
history = []
|
||||
}
|
||||
|
||||
@@ -220,8 +225,8 @@ extension TrackingSubscriberBase.Event {
|
||||
switch (lhs, rhs) {
|
||||
case (.finished, .finished):
|
||||
return true
|
||||
case let (.failure(lhs), .failure(rhs)):
|
||||
return (lhs as? TestingError) == (rhs as? TestingError)
|
||||
case let (.failure(lhs as EquatableError), .failure(rhs as EquatableError)):
|
||||
return lhs.isEqual(rhs)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
@@ -277,8 +282,9 @@ final class TrackingSubjectBase<Output: Equatable, Failure: Error>
|
||||
switch (lhs, rhs) {
|
||||
case (.finished, .finished):
|
||||
return true
|
||||
case let (.failure(lhs), .failure(rhs)):
|
||||
return (lhs as? TestingError) == (rhs as? TestingError)
|
||||
case let (.failure(lhs as EquatableError),
|
||||
.failure(rhs as EquatableError)):
|
||||
return lhs.isEqual(rhs)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -0,0 +1,316 @@
|
||||
//
|
||||
// VirtualTimeScheduler.swift
|
||||
// OpenCombineTests
|
||||
//
|
||||
// Created by Евгений Богомолов on 14/09/2019.
|
||||
//
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class VirtualTimeScheduler: Scheduler {
|
||||
|
||||
struct SchedulerTimeType: Strideable,
|
||||
Comparable,
|
||||
Hashable,
|
||||
SchedulerTimeIntervalConvertible
|
||||
{
|
||||
|
||||
struct Stride: ExpressibleByFloatLiteral,
|
||||
Comparable,
|
||||
SignedNumeric,
|
||||
SchedulerTimeIntervalConvertible
|
||||
{
|
||||
var magnitude: Int64
|
||||
|
||||
fileprivate init(magnitude: Int64) {
|
||||
self.magnitude = magnitude
|
||||
}
|
||||
|
||||
init(integerLiteral value: Int) {
|
||||
self = .seconds(value)
|
||||
}
|
||||
|
||||
init(floatLiteral value: Double) {
|
||||
self = .seconds(value)
|
||||
}
|
||||
|
||||
init?<Source: BinaryInteger>(exactly source: Source) {
|
||||
guard let magnitude = Int64(exactly: source) else {
|
||||
return nil
|
||||
}
|
||||
self.init(magnitude: magnitude)
|
||||
}
|
||||
|
||||
static func < (lhs: Stride, rhs: Stride) -> Bool {
|
||||
return lhs.magnitude < rhs.magnitude
|
||||
}
|
||||
|
||||
static func * (lhs: Stride, rhs: Stride) -> Stride {
|
||||
return Stride(magnitude: lhs.magnitude * rhs.magnitude)
|
||||
}
|
||||
|
||||
static func + (lhs: Stride, rhs: Stride) -> Stride {
|
||||
return Stride(magnitude: lhs.magnitude + rhs.magnitude)
|
||||
}
|
||||
|
||||
static func - (lhs: Stride, rhs: Stride) -> Stride {
|
||||
return Stride(magnitude: lhs.magnitude - rhs.magnitude)
|
||||
}
|
||||
|
||||
static func -= (lhs: inout Stride, rhs: Stride) {
|
||||
lhs.magnitude -= rhs.magnitude
|
||||
}
|
||||
|
||||
static func *= (lhs: inout Stride, rhs: Stride) {
|
||||
lhs.magnitude *= rhs.magnitude
|
||||
}
|
||||
|
||||
static func += (lhs: inout Stride, rhs: Stride) {
|
||||
lhs.magnitude += rhs.magnitude
|
||||
}
|
||||
|
||||
static func seconds(_ value: Int) -> Stride {
|
||||
return Stride(magnitude: Int64(value) * 1_000_000_000)
|
||||
}
|
||||
|
||||
static func seconds(_ value: Double) -> Stride {
|
||||
return Stride(magnitude: Int64(value * 1_000_000_000))
|
||||
}
|
||||
|
||||
static func milliseconds(_ value: Int) -> Stride {
|
||||
return Stride(magnitude: Int64(value) * 1_000_000)
|
||||
}
|
||||
|
||||
static func microseconds(_ value: Int) -> Stride {
|
||||
return Stride(magnitude: Int64(value) * 1_000)
|
||||
}
|
||||
|
||||
static func nanoseconds(_ value: Int) -> Stride {
|
||||
return Stride(magnitude: Int64(value))
|
||||
}
|
||||
}
|
||||
|
||||
/// Time in virtual nanoseconds
|
||||
let time: UInt64
|
||||
|
||||
private init(nanoseconds time: UInt64) {
|
||||
self.time = time
|
||||
}
|
||||
|
||||
static func == (lhs: SchedulerTimeType, rhs: SchedulerTimeType) -> Bool {
|
||||
return lhs.time == rhs.time
|
||||
}
|
||||
|
||||
static func < (lhs: SchedulerTimeType, rhs: SchedulerTimeType) -> Bool {
|
||||
return lhs.time < rhs.time
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(time)
|
||||
}
|
||||
|
||||
func distance(to other: SchedulerTimeType) -> Stride {
|
||||
if self > other {
|
||||
return Stride(magnitude: -Int64(time - other.time))
|
||||
} else {
|
||||
return Stride(magnitude: Int64(other.time - time))
|
||||
}
|
||||
}
|
||||
|
||||
func advanced(by stride: Stride) -> SchedulerTimeType {
|
||||
return stride.magnitude < 0
|
||||
? SchedulerTimeType(nanoseconds: time - UInt64(-stride.magnitude))
|
||||
: SchedulerTimeType(nanoseconds: time + UInt64(stride.magnitude))
|
||||
}
|
||||
|
||||
static func + (lhs: SchedulerTimeType, rhs: Stride) -> SchedulerTimeType {
|
||||
return lhs.advanced(by: rhs)
|
||||
}
|
||||
|
||||
static let beginningOfTime = SchedulerTimeType(nanoseconds: 0)
|
||||
|
||||
static func seconds(_ value: Int) -> SchedulerTimeType {
|
||||
precondition(value >= 0, "value must not be negative")
|
||||
return .init(nanoseconds: UInt64(value) * 1_000_000_000)
|
||||
}
|
||||
|
||||
static func seconds(_ value: Double) -> SchedulerTimeType {
|
||||
precondition(value >= 0, "value must not be negative")
|
||||
return .init(nanoseconds: UInt64(value * 1_000_000_000))
|
||||
}
|
||||
|
||||
static func milliseconds(_ value: Int) -> SchedulerTimeType {
|
||||
precondition(value >= 0, "value must not be negative")
|
||||
return .init(nanoseconds: UInt64(value) * 1_000_000)
|
||||
}
|
||||
|
||||
static func microseconds(_ value: Int) -> SchedulerTimeType {
|
||||
precondition(value >= 0, "value must not be negative")
|
||||
return .init(nanoseconds: UInt64(value) * 1_000)
|
||||
}
|
||||
|
||||
static func nanoseconds(_ value: Int) -> SchedulerTimeType {
|
||||
precondition(value >= 0, "value must not be negative")
|
||||
return .init(nanoseconds: UInt64(value))
|
||||
}
|
||||
}
|
||||
|
||||
enum SchedulerOptions: Equatable, CustomStringConvertible {
|
||||
case nontrivialOptions
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .nontrivialOptions:
|
||||
return ".nontrivialOptions"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class CancellableToken: Cancellable {
|
||||
|
||||
private(set) var isCancelled = false
|
||||
|
||||
func cancel() {
|
||||
isCancelled = true
|
||||
}
|
||||
}
|
||||
|
||||
enum Event: Equatable, CustomStringConvertible {
|
||||
case now
|
||||
case minimumTolerance
|
||||
case schedule(options: SchedulerOptions?)
|
||||
case scheduleAfterDate(SchedulerTimeType,
|
||||
tolerance: SchedulerTimeType.Stride,
|
||||
options: SchedulerOptions?)
|
||||
case scheduleAfterDateWithInterval(SchedulerTimeType,
|
||||
interval: SchedulerTimeType.Stride,
|
||||
tolerance: SchedulerTimeType.Stride,
|
||||
options: SchedulerOptions?)
|
||||
|
||||
var description: String {
|
||||
|
||||
func describeOptions(_ options: SchedulerOptions?) -> String {
|
||||
return options.map(String.init(describing:)) ?? "nil"
|
||||
}
|
||||
|
||||
func describeDate(_ date: SchedulerTimeType) -> String {
|
||||
return ".nanoseconds(\(date.time)"
|
||||
}
|
||||
|
||||
func describeStride(_ stride: SchedulerTimeType.Stride) -> String {
|
||||
return ".nanoseconds(\(stride.magnitude))"
|
||||
}
|
||||
|
||||
switch self {
|
||||
case .now:
|
||||
return ".now"
|
||||
case .minimumTolerance:
|
||||
return ".minimumTolerance"
|
||||
case let .schedule(options):
|
||||
return ".schedule(options: \(describeOptions(options)))"
|
||||
case let .scheduleAfterDate(date, tolerance, options):
|
||||
return """
|
||||
.scheduleAfterDate(\(describeDate(date)), \
|
||||
tolerance: \(describeStride(tolerance)), \
|
||||
options: \(describeOptions(options)))
|
||||
"""
|
||||
case let .scheduleAfterDateWithInterval(date, interval, tolerance, options):
|
||||
return """
|
||||
.scheduleAfterDateWithInterval(\(describeDate(date)), \
|
||||
interval: \(describeStride(interval)), \
|
||||
tolerance: \(describeStride(tolerance)), \
|
||||
options: \(describeOptions(options)))
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private(set) var history = [Event]()
|
||||
|
||||
/// All private methods should reference this property instead of `now`
|
||||
/// to prevent polluting the scheduler history. Accessing `now` creates an entry
|
||||
/// in the `history` array.
|
||||
private var _now = SchedulerTimeType.beginningOfTime
|
||||
|
||||
private var workQueue = FairPriorityQueue<SchedulerTimeType, () -> Void>()
|
||||
|
||||
var scheduledDates: [SchedulerTimeType] {
|
||||
return workQueue.map { $0.0 }
|
||||
}
|
||||
|
||||
var now: SchedulerTimeType {
|
||||
history.append(.now)
|
||||
return _now
|
||||
}
|
||||
|
||||
var minimumTolerance: SchedulerTimeType.Stride {
|
||||
history.append(.minimumTolerance)
|
||||
return 0
|
||||
}
|
||||
|
||||
func schedule(options: SchedulerOptions?, _ action: @escaping () -> Void) {
|
||||
history.append(.schedule(options: options))
|
||||
workQueue.insert(action, priority: _now)
|
||||
}
|
||||
|
||||
func schedule(after date: SchedulerTimeType,
|
||||
tolerance: SchedulerTimeType.Stride,
|
||||
options: SchedulerOptions?,
|
||||
_ action: @escaping () -> Void) {
|
||||
history.append(.scheduleAfterDate(date, tolerance: tolerance, options: options))
|
||||
workQueue.insert(action, priority: date)
|
||||
}
|
||||
|
||||
func schedule(after date: SchedulerTimeType,
|
||||
interval: SchedulerTimeType.Stride,
|
||||
tolerance: SchedulerTimeType.Stride,
|
||||
options: SchedulerOptions?,
|
||||
_ action: @escaping () -> Void) -> Cancellable {
|
||||
history.append(.scheduleAfterDateWithInterval(date,
|
||||
interval: interval,
|
||||
tolerance: tolerance,
|
||||
options: options))
|
||||
let cancellableToken = CancellableToken()
|
||||
repeatedlyExecute(after: date,
|
||||
interval: interval,
|
||||
cancellableToken: cancellableToken,
|
||||
action: action)
|
||||
return cancellableToken
|
||||
}
|
||||
|
||||
private func repeatedlyExecute(after date: SchedulerTimeType,
|
||||
interval: SchedulerTimeType.Stride,
|
||||
cancellableToken: CancellableToken,
|
||||
action: @escaping () -> Void) {
|
||||
let enqueuedAction: () -> Void = { [unowned self] in
|
||||
if cancellableToken.isCancelled { return }
|
||||
action()
|
||||
self.repeatedlyExecute(after: date + interval,
|
||||
interval: interval,
|
||||
cancellableToken: cancellableToken,
|
||||
action: action)
|
||||
}
|
||||
workQueue.insert(enqueuedAction, priority: date)
|
||||
}
|
||||
|
||||
/// Sets `now` to the provided value. Useful for testing that an entity that
|
||||
/// uses the scheduler doesn't rely on clock monotonicity.
|
||||
///
|
||||
/// - Note: The actions that were already executed will not be executed again.
|
||||
/// This function does **not** provide time machine-like functionality.
|
||||
func rewind(to time: SchedulerTimeType) {
|
||||
_now = time
|
||||
}
|
||||
|
||||
func executeScheduledActions() {
|
||||
while let (time, action) = workQueue.extractMin() {
|
||||
_now = max(time, _now)
|
||||
action()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,23 @@
|
||||
|
||||
import XCTest
|
||||
|
||||
// FIXME: XCTUnwrap is unavailable in Swift Package Manager yet.
|
||||
// XCTUnwrap is not available when using Swift Package Manager.
|
||||
// This has been fixed in Swift 5.2, but we want to maintain compatibility.
|
||||
#if swift(<5.2)
|
||||
|
||||
private struct UnwrappingFailure: Error {}
|
||||
private struct UnwrappingFailure: Error, LocalizedError {
|
||||
|
||||
let message: String
|
||||
|
||||
var errorDescription: String? {
|
||||
var failureDescription = "XCTUnwrap failed"
|
||||
if !message.isEmpty {
|
||||
failureDescription += ": "
|
||||
failureDescription += message
|
||||
}
|
||||
return failureDescription
|
||||
}
|
||||
}
|
||||
|
||||
/// Asserts that an expression is not `nil`, and returns its unwrapped value.
|
||||
///
|
||||
@@ -32,10 +46,12 @@ public func XCTUnwrap<Result>(_ expression: @autoclosure () throws -> Result?,
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line) throws -> Result {
|
||||
let result = try expression()
|
||||
XCTAssertNotNil(result, message(), file: file, line: line)
|
||||
if let result = result {
|
||||
return result
|
||||
} else {
|
||||
throw UnwrappingFailure()
|
||||
let error = UnwrappingFailure(message: message())
|
||||
XCTFail(error.errorDescription ?? "", file: file, line: line)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -23,22 +23,27 @@ final class ObservableObjectPublisherTests: XCTestCase {
|
||||
receiveSubscription: { downstreamSubscription1 = $0 }
|
||||
)
|
||||
publisher.subscribe(tracking1)
|
||||
tracking1.assertHistoryEqual([.subscription("PassthroughSubject")])
|
||||
tracking1.assertHistoryEqual([.subscription("ObservableObjectPublisher")])
|
||||
downstreamSubscription1?.request(.max(1))
|
||||
tracking1.assertHistoryEqual([.subscription("PassthroughSubject")])
|
||||
tracking1.assertHistoryEqual([.subscription("ObservableObjectPublisher")])
|
||||
publisher.send()
|
||||
tracking1.assertHistoryEqual([.subscription("PassthroughSubject"),
|
||||
tracking1.assertHistoryEqual([.subscription("ObservableObjectPublisher"),
|
||||
.signal])
|
||||
publisher.send()
|
||||
publisher.send()
|
||||
downstreamSubscription1?.request(.max(3))
|
||||
tracking1.assertHistoryEqual([.subscription("PassthroughSubject"),
|
||||
tracking1.assertHistoryEqual([.subscription("ObservableObjectPublisher"),
|
||||
.signal,
|
||||
.signal,
|
||||
.signal])
|
||||
publisher.send()
|
||||
publisher.send()
|
||||
publisher.send()
|
||||
publisher.send()
|
||||
tracking1.assertHistoryEqual([.subscription("PassthroughSubject"),
|
||||
tracking1.assertHistoryEqual([.subscription("ObservableObjectPublisher"),
|
||||
.signal,
|
||||
.signal,
|
||||
.signal,
|
||||
.signal,
|
||||
.signal,
|
||||
.signal,
|
||||
@@ -49,28 +54,48 @@ final class ObservableObjectPublisherTests: XCTestCase {
|
||||
receiveSubscription: { $0.request(.unlimited) }
|
||||
)
|
||||
publisher.subscribe(tracking2)
|
||||
tracking2.assertHistoryEqual([.subscription("PassthroughSubject")])
|
||||
tracking2.assertHistoryEqual([.subscription("ObservableObjectPublisher")])
|
||||
|
||||
publisher.send()
|
||||
tracking1.assertHistoryEqual([.subscription("PassthroughSubject"),
|
||||
tracking1.assertHistoryEqual([.subscription("ObservableObjectPublisher"),
|
||||
.signal,
|
||||
.signal,
|
||||
.signal,
|
||||
.signal,
|
||||
.signal,
|
||||
.signal,
|
||||
.signal,
|
||||
.signal])
|
||||
tracking2.assertHistoryEqual([.subscription("PassthroughSubject"),
|
||||
tracking2.assertHistoryEqual([.subscription("ObservableObjectPublisher"),
|
||||
.signal])
|
||||
|
||||
downstreamSubscription1?.cancel()
|
||||
publisher.send()
|
||||
tracking1.assertHistoryEqual([.subscription("PassthroughSubject"),
|
||||
tracking1.assertHistoryEqual([.subscription("ObservableObjectPublisher"),
|
||||
.signal,
|
||||
.signal,
|
||||
.signal,
|
||||
.signal,
|
||||
.signal,
|
||||
.signal,
|
||||
.signal,
|
||||
.signal])
|
||||
tracking2.assertHistoryEqual([.subscription("PassthroughSubject"),
|
||||
tracking2.assertHistoryEqual([.subscription("ObservableObjectPublisher"),
|
||||
.signal,
|
||||
.signal])
|
||||
|
||||
tracking1.cancel()
|
||||
tracking2.cancel()
|
||||
}
|
||||
|
||||
func testObservableObjectPublisherReflection() throws {
|
||||
try testSubscriptionReflection(
|
||||
description: "ObservableObjectPublisher",
|
||||
customMirror: expectedChildren(
|
||||
("downstream", .contains("TrackingSubscriberBase"))
|
||||
),
|
||||
playgroundDescription: "ObservableObjectPublisher",
|
||||
sut: ObservableObjectPublisher()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,11 +88,11 @@ final class PublishedTests: XCTestCase {
|
||||
receiveSubscription: { downstreamSubscription = $0 }
|
||||
)
|
||||
testObject.objectWillChange.subscribe(tracking1)
|
||||
tracking1.assertHistoryEqual([.subscription("PassthroughSubject")])
|
||||
tracking1.assertHistoryEqual([.subscription("ObservableObjectPublisher")])
|
||||
downstreamSubscription?.request(.max(2))
|
||||
tracking1.assertHistoryEqual([.subscription("PassthroughSubject")])
|
||||
tracking1.assertHistoryEqual([.subscription("ObservableObjectPublisher")])
|
||||
testObject.state = 100
|
||||
tracking1.assertHistoryEqual([.subscription("PassthroughSubject")])
|
||||
tracking1.assertHistoryEqual([.subscription("ObservableObjectPublisher")])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
//
|
||||
// AssertNoFailureTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 25.12.2019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class AssertNoFailureTests: XCTestCase {
|
||||
|
||||
func testPassThroughInput() throws {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .max(2),
|
||||
createSut: { $0.assertNoFailure() })
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(1), .max(2))
|
||||
XCTAssertEqual(helper.publisher.send(2), .max(2))
|
||||
XCTAssertEqual(helper.publisher.send(3), .max(2))
|
||||
helper.publisher.send(completion: .finished)
|
||||
helper.publisher.send(completion: .finished)
|
||||
|
||||
let subscription2 = CustomSubscription()
|
||||
helper.publisher.send(subscription: subscription2)
|
||||
helper.publisher.send(subscription: subscription2)
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(42))
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("CustomSubscription"),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3),
|
||||
.completion(.finished),
|
||||
.completion(.finished),
|
||||
.subscription("CustomSubscription"),
|
||||
.subscription("CustomSubscription")])
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [])
|
||||
|
||||
XCTAssertEqual(subscription2.history, [.requested(.max(42)),
|
||||
.cancelled,
|
||||
.cancelled])
|
||||
}
|
||||
|
||||
func testCrashesOnFailure() {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .max(2),
|
||||
createSut: { $0.assertNoFailure() })
|
||||
helper.publisher.send(completion: .finished)
|
||||
assertCrashes {
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
}
|
||||
}
|
||||
|
||||
func testAssertNoFailureReflection() throws {
|
||||
try testReflection(
|
||||
parentInput: Int.self,
|
||||
parentFailure: TestingError.self,
|
||||
description: "AssertNoFailure",
|
||||
customMirror: expectedChildren(
|
||||
("file", "SomeFile.swift"),
|
||||
("line", "1987"),
|
||||
("prefix", "PREFIX")
|
||||
),
|
||||
playgroundDescription: "AssertNoFailure",
|
||||
subscriberIsAlsoSubscription: false,
|
||||
{ $0.assertNoFailure("PREFIX", file: "SomeFile.swift", line: 1987) }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -151,6 +151,7 @@ final class AutoconnectTests: XCTestCase {
|
||||
description: "Autoconnect",
|
||||
customMirror: customMirrorPredicate,
|
||||
playgroundDescription: "Autoconnect",
|
||||
subscriberIsAlsoSubscription: false,
|
||||
{ $0.autoconnect() })
|
||||
|
||||
let subscription = CustomSubscription()
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
//
|
||||
// BreakpointTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 03.12.2019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class BreakpointTests: XCTestCase {
|
||||
|
||||
func testReceiveSubscription() {
|
||||
var shouldStop = false
|
||||
var counter = 0
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .max(1)) {
|
||||
$0.breakpoint(receiveSubscription: { _ in counter += 1; return shouldStop })
|
||||
}
|
||||
|
||||
XCTAssertNotNil(helper.sut.receiveSubscription)
|
||||
XCTAssertNil(helper.sut.receiveOutput)
|
||||
XCTAssertNil(helper.sut.receiveCompletion)
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(12), .max(1))
|
||||
helper.publisher.send(completion: .finished)
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
XCTAssertEqual(helper.publisher.send(21), .max(1))
|
||||
helper.publisher.send(subscription: CustomSubscription())
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("CustomSubscription"),
|
||||
.value(12),
|
||||
.completion(.finished),
|
||||
.completion(.failure(.oops)),
|
||||
.value(21),
|
||||
.subscription("CustomSubscription")])
|
||||
XCTAssertEqual(helper.subscription.history, [])
|
||||
shouldStop = true
|
||||
XCTAssertEqual(counter, 2)
|
||||
assertCrashes {
|
||||
helper.publisher.send(subscription: CustomSubscription())
|
||||
}
|
||||
}
|
||||
|
||||
func testReceiveValue() {
|
||||
var counter = 0
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .max(1)) {
|
||||
$0.breakpoint(receiveOutput: { counter += 1; return $0 < 0 })
|
||||
}
|
||||
|
||||
XCTAssertNil(helper.sut.receiveSubscription)
|
||||
XCTAssertNotNil(helper.sut.receiveOutput)
|
||||
XCTAssertNil(helper.sut.receiveCompletion)
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(12), .max(1))
|
||||
helper.publisher.send(completion: .finished)
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
XCTAssertEqual(helper.publisher.send(21), .max(1))
|
||||
helper.publisher.send(subscription: CustomSubscription())
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("CustomSubscription"),
|
||||
.value(12),
|
||||
.completion(.finished),
|
||||
.completion(.failure(.oops)),
|
||||
.value(21),
|
||||
.subscription("CustomSubscription")])
|
||||
XCTAssertEqual(helper.subscription.history, [])
|
||||
XCTAssertEqual(counter, 2)
|
||||
assertCrashes {
|
||||
_ = helper.publisher.send(-1)
|
||||
}
|
||||
}
|
||||
|
||||
func testReceiveCompletion() {
|
||||
var counter = 0
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .max(1)) {
|
||||
$0.breakpoint(receiveCompletion: { counter += 1; return $0 == .finished })
|
||||
}
|
||||
|
||||
XCTAssertNil(helper.sut.receiveSubscription)
|
||||
XCTAssertNil(helper.sut.receiveOutput)
|
||||
XCTAssertNotNil(helper.sut.receiveCompletion)
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(12), .max(1))
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
XCTAssertEqual(helper.publisher.send(21), .max(1))
|
||||
helper.publisher.send(subscription: CustomSubscription())
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("CustomSubscription"),
|
||||
.value(12),
|
||||
.completion(.failure(.oops)),
|
||||
.completion(.failure(.oops)),
|
||||
.value(21),
|
||||
.subscription("CustomSubscription")])
|
||||
XCTAssertEqual(counter, 2)
|
||||
assertCrashes {
|
||||
helper.publisher.send(completion: .finished)
|
||||
}
|
||||
}
|
||||
|
||||
func testBreakpointOnError() throws {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .max(1)) {
|
||||
$0.breakpointOnError()
|
||||
}
|
||||
|
||||
XCTAssertNil(helper.sut.receiveSubscription)
|
||||
XCTAssertNil(helper.sut.receiveOutput)
|
||||
XCTAssertNotNil(helper.sut.receiveCompletion)
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(12), .max(1))
|
||||
helper.publisher.send(completion: .finished)
|
||||
helper.publisher.send(completion: .finished)
|
||||
XCTAssertEqual(helper.publisher.send(21), .max(1))
|
||||
helper.publisher.send(subscription: CustomSubscription())
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("CustomSubscription"),
|
||||
.value(12),
|
||||
.completion(.finished),
|
||||
.completion(.finished),
|
||||
.value(21),
|
||||
.subscription("CustomSubscription")])
|
||||
|
||||
XCTAssertEqual(helper.sut.receiveCompletion?(.finished), false)
|
||||
XCTAssertEqual(helper.sut.receiveCompletion?(.failure(.oops)), true)
|
||||
|
||||
assertCrashes {
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
}
|
||||
}
|
||||
|
||||
func testCancelAlreadyCancelled() throws {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .none) {
|
||||
$0.breakpointOnError()
|
||||
}
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(14))
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(100))
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
|
||||
.requested(.max(14)),
|
||||
.cancelled,
|
||||
.requested(.max(100)),
|
||||
.cancelled])
|
||||
}
|
||||
|
||||
func testBreakpointReflection() throws {
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: Error.self,
|
||||
description: "Breakpoint",
|
||||
customMirror: expectedChildren(
|
||||
("upstream", .contains("CustomConnectablePublisherBase"))
|
||||
),
|
||||
playgroundDescription: "Breakpoint",
|
||||
subscriberIsAlsoSubscription: false,
|
||||
{ $0.breakpointOnError() })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,953 @@
|
||||
//
|
||||
// BufferTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 08.01.2020.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class BufferTests: XCTestCase {
|
||||
|
||||
func testInitialDemandWithKeepFullPrefetchStrategy() {
|
||||
testInitialDemand(
|
||||
withPrefetchStragety: .keepFull,
|
||||
expectedSubscriptionHistory: [.requested(.max(42))]
|
||||
)
|
||||
}
|
||||
|
||||
func testInitialDemandWithByRequestPrefetchStrategy() {
|
||||
testInitialDemand(
|
||||
withPrefetchStragety: .byRequest,
|
||||
expectedSubscriptionHistory: [.requested(.unlimited)]
|
||||
)
|
||||
}
|
||||
|
||||
func testBufferingInputDroppingNewest() throws {
|
||||
try testBufferingInput(whenFull: .dropNewest)
|
||||
}
|
||||
|
||||
func testBufferingInputDroppingOldest() throws {
|
||||
try testBufferingInput(whenFull: .dropOldest)
|
||||
}
|
||||
|
||||
func testBufferingInputFailingWhenBufferIsFull() throws {
|
||||
try testBufferingInput(whenFull: .customError { .oops })
|
||||
}
|
||||
|
||||
func testReceiveValueAfterFinishing() throws {
|
||||
try testReceiveValueAfterCompleting(.finished)
|
||||
}
|
||||
|
||||
func testReceiveValueAfterFailing() throws {
|
||||
try testReceiveValueAfterCompleting(.failure(.oops))
|
||||
}
|
||||
|
||||
func testDeadlockWhenErroringOnFullBuffer() {
|
||||
var recursionCounter = 10
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(2),
|
||||
receiveValueDemand: .none,
|
||||
createSut: { publisher in
|
||||
publisher.buffer(
|
||||
size: 0,
|
||||
prefetch: .keepFull,
|
||||
whenFull: .customError {
|
||||
if recursionCounter == 0 { return TestingError.oops }
|
||||
recursionCounter -= 1
|
||||
_ = publisher.send(1000)
|
||||
return TestingError.oops
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
assertCrashes {
|
||||
_ = helper.publisher.send(0)
|
||||
}
|
||||
}
|
||||
|
||||
func testRecursionByRequestDropNewest() {
|
||||
testRecursion(prefetch: .byRequest, whenFull: .dropNewest)
|
||||
}
|
||||
|
||||
func testRecursionByRequestDropOldest() {
|
||||
testRecursion(prefetch: .byRequest, whenFull: .dropOldest)
|
||||
}
|
||||
|
||||
func testRecursionByRequestCustomError() {
|
||||
testRecursion(prefetch: .byRequest, whenFull: .customError { .oops })
|
||||
}
|
||||
|
||||
func testRecursionKeepFullDropNewest() {
|
||||
testRecursion(prefetch: .keepFull, whenFull: .dropNewest)
|
||||
}
|
||||
|
||||
func testRecursionKeepFullDropOldest() {
|
||||
testRecursion(prefetch: .keepFull, whenFull: .dropOldest)
|
||||
}
|
||||
|
||||
func testRecursionKeepFullCustomError() {
|
||||
testRecursion(prefetch: .keepFull, whenFull: .customError { .oops })
|
||||
}
|
||||
|
||||
func testRequestingUnlimitedDemandByRequestDropNewest() {
|
||||
testRequestingUnlimitedDemand(prefetch: .byRequest,
|
||||
whenFull: .dropNewest)
|
||||
}
|
||||
|
||||
func testRequestingUnlimitedDemandByRequestDropOldest() {
|
||||
testRequestingUnlimitedDemand(prefetch: .byRequest,
|
||||
whenFull: .dropOldest)
|
||||
}
|
||||
|
||||
func testRequestingUnlimitedDemandByRequestCustomError() {
|
||||
testRequestingUnlimitedDemand(prefetch: .byRequest,
|
||||
whenFull: .customError { .oops })
|
||||
}
|
||||
|
||||
func testRequestingUnlimitedDemandKeepFullDropNewest() {
|
||||
testRequestingUnlimitedDemand(prefetch: .keepFull,
|
||||
whenFull: .dropNewest)
|
||||
}
|
||||
|
||||
func testRequestingUnlimitedDemandKeepFullDropOldest() {
|
||||
testRequestingUnlimitedDemand(prefetch: .keepFull,
|
||||
whenFull: .dropOldest)
|
||||
}
|
||||
|
||||
func testRequestingUnlimitedDemandKeepFullCustomError() {
|
||||
testRequestingUnlimitedDemand(prefetch: .keepFull,
|
||||
whenFull: .customError { .oops })
|
||||
}
|
||||
|
||||
func testRequestingFiniteDemandByRequestDropNewest() {
|
||||
testRequestingFiniteDemand(prefetch: .byRequest, whenFull: .dropNewest)
|
||||
}
|
||||
|
||||
func testRequestingFiniteDemandByRequestDropOldest() {
|
||||
testRequestingFiniteDemand(prefetch: .byRequest, whenFull: .dropOldest)
|
||||
}
|
||||
|
||||
func testRequestingFiniteDemandByRequestCustomError() {
|
||||
testRequestingFiniteDemand(prefetch: .byRequest, whenFull: .customError { .oops })
|
||||
}
|
||||
|
||||
func testRequestingFiniteDemandKeepFullDropNewest() {
|
||||
testRequestingFiniteDemand(prefetch: .keepFull, whenFull: .dropNewest)
|
||||
}
|
||||
|
||||
func testRequestingFiniteDemandKeepFullDropOldest() {
|
||||
testRequestingFiniteDemand(prefetch: .keepFull, whenFull: .dropOldest)
|
||||
}
|
||||
|
||||
func testRequestingFiniteDemandKeepFullCustomError() {
|
||||
testRequestingFiniteDemand(prefetch: .keepFull, whenFull: .customError { .oops })
|
||||
}
|
||||
|
||||
func testBufferByRequestDropNewestReceiveSubscriptionTwice() throws {
|
||||
try testBufferReceiveSubscriptionTwice(prefetch: .byRequest,
|
||||
whenFull: .dropNewest)
|
||||
}
|
||||
|
||||
func testBufferByRequestDropOldestReceiveSubscriptionTwice() throws {
|
||||
try testBufferReceiveSubscriptionTwice(prefetch: .byRequest,
|
||||
whenFull: .dropOldest)
|
||||
}
|
||||
|
||||
func testBufferByRequestCustomErrorReceiveSubscriptionTwice() throws {
|
||||
try testBufferReceiveSubscriptionTwice(prefetch: .byRequest,
|
||||
whenFull: .customError { .oops })
|
||||
}
|
||||
|
||||
func testBufferKeepFullDropNewestReceiveSubscriptionTwice() throws {
|
||||
try testBufferReceiveSubscriptionTwice(prefetch: .keepFull,
|
||||
whenFull: .dropNewest)
|
||||
}
|
||||
|
||||
func testBufferKeepFullDropOldestReceiveSubscriptionTwice() throws {
|
||||
try testBufferReceiveSubscriptionTwice(prefetch: .keepFull,
|
||||
whenFull: .dropOldest)
|
||||
}
|
||||
|
||||
func testBufferKeepFullCustomErrorReceiveSubscriptionTwice() throws {
|
||||
try testBufferReceiveSubscriptionTwice(prefetch: .keepFull,
|
||||
whenFull: .customError { .oops })
|
||||
}
|
||||
|
||||
func testBufferByRequestDropNewestReceiveValueBeforeSubscription() {
|
||||
testBufferReceiveValueBeforeSubscription(prefetch: .byRequest,
|
||||
whenFull: .dropNewest)
|
||||
}
|
||||
|
||||
func testBufferByRequestDropOldestReceiveValueBeforeSubscription() {
|
||||
testBufferReceiveValueBeforeSubscription(prefetch: .byRequest,
|
||||
whenFull: .dropOldest)
|
||||
}
|
||||
|
||||
func testBufferByRequestCustomErrorReceiveValueBeforeSubscription() {
|
||||
testBufferReceiveValueBeforeSubscription(prefetch: .byRequest,
|
||||
whenFull: .customError(unreachable))
|
||||
}
|
||||
|
||||
func testBufferKeepFullDropNewestReceiveValueBeforeSubscription() {
|
||||
testBufferReceiveValueBeforeSubscription(prefetch: .keepFull,
|
||||
whenFull: .dropNewest)
|
||||
}
|
||||
|
||||
func testBufferKeepFullDropOldestReceiveValueBeforeSubscription() {
|
||||
testBufferReceiveValueBeforeSubscription(prefetch: .keepFull,
|
||||
whenFull: .dropOldest)
|
||||
}
|
||||
|
||||
func testBufferKeepFullCustomErrorReceiveValueBeforeSubscription() {
|
||||
testBufferReceiveValueBeforeSubscription(prefetch: .keepFull,
|
||||
whenFull: .customError(unreachable))
|
||||
}
|
||||
|
||||
func testBufferByRequestDropNewestReceiveCompletionBeforeSubscription() {
|
||||
testBufferReceiveCompletionBeforeSubscription(prefetch: .byRequest,
|
||||
whenFull: .dropNewest)
|
||||
}
|
||||
|
||||
func testBufferByRequestDropOldestReceiveCompletionBeforeSubscription() {
|
||||
testBufferReceiveCompletionBeforeSubscription(prefetch: .byRequest,
|
||||
whenFull: .dropOldest)
|
||||
}
|
||||
|
||||
func testBufferByRequestCustomErrorReceiveCompletionBeforeSubscription() {
|
||||
testBufferReceiveCompletionBeforeSubscription(prefetch: .byRequest,
|
||||
whenFull: .customError(unreachable))
|
||||
}
|
||||
|
||||
func testBufferKeepFullDropNewestReceiveCompletionBeforeSubscription() {
|
||||
testBufferReceiveCompletionBeforeSubscription(prefetch: .keepFull,
|
||||
whenFull: .dropNewest)
|
||||
}
|
||||
|
||||
func testBufferKeepFullDropOldestReceiveCompletionBeforeSubscription() {
|
||||
testBufferReceiveCompletionBeforeSubscription(prefetch: .keepFull,
|
||||
whenFull: .dropOldest)
|
||||
}
|
||||
|
||||
func testBufferKeepFullCustomErrorReceiveCompletionBeforeSubscription() {
|
||||
testBufferReceiveCompletionBeforeSubscription(prefetch: .keepFull,
|
||||
whenFull: .customError(unreachable))
|
||||
}
|
||||
|
||||
func testBufferByRequestDropNewestRequestBeforeSubscription() {
|
||||
testBufferRequestBeforeSubscription(prefetch: .byRequest,
|
||||
whenFull: .dropNewest)
|
||||
}
|
||||
|
||||
func testBufferByRequestDropOldestRequestBeforeSubscription() {
|
||||
testBufferRequestBeforeSubscription(prefetch: .byRequest,
|
||||
whenFull: .dropOldest)
|
||||
}
|
||||
|
||||
func testBufferByRequestCustomErrorRequestBeforeSubscription() {
|
||||
testBufferRequestBeforeSubscription(prefetch: .byRequest,
|
||||
whenFull: .customError(unreachable))
|
||||
}
|
||||
|
||||
func testBufferKeepFullDropNewestRequestBeforeSubscription() {
|
||||
testBufferRequestBeforeSubscription(prefetch: .keepFull,
|
||||
whenFull: .dropNewest)
|
||||
}
|
||||
|
||||
func testBufferKeepFullDropOldestRequestBeforeSubscription() {
|
||||
testBufferRequestBeforeSubscription(prefetch: .keepFull,
|
||||
whenFull: .dropOldest)
|
||||
}
|
||||
|
||||
func testBufferKeepFullCustomErrorRequestBeforeSubscription() {
|
||||
testBufferRequestBeforeSubscription(prefetch: .keepFull,
|
||||
whenFull: .customError(unreachable))
|
||||
}
|
||||
|
||||
func testBufferByRequestDropNewestCancelBeforeSubscription() {
|
||||
testBufferCancelBeforeSubscription(prefetch: .byRequest,
|
||||
whenFull: .dropNewest)
|
||||
}
|
||||
|
||||
func testBufferByRequestDropOldestCancelBeforeSubscription() {
|
||||
testBufferCancelBeforeSubscription(prefetch: .byRequest,
|
||||
whenFull: .dropOldest)
|
||||
}
|
||||
|
||||
func testBufferByRequestCustomErrorCancelBeforeSubscription() {
|
||||
testBufferCancelBeforeSubscription(prefetch: .byRequest,
|
||||
whenFull: .customError(unreachable))
|
||||
}
|
||||
|
||||
func testBufferKeepFullDropNewestCancelBeforeSubscription() {
|
||||
testBufferCancelBeforeSubscription(prefetch: .keepFull,
|
||||
whenFull: .dropNewest)
|
||||
}
|
||||
|
||||
func testBufferKeepFullDropOldestCancelBeforeSubscription() {
|
||||
testBufferCancelBeforeSubscription(prefetch: .keepFull,
|
||||
whenFull: .dropOldest)
|
||||
}
|
||||
|
||||
func testBufferKeepFullCustomErrorCancelBeforeSubscription() {
|
||||
testBufferCancelBeforeSubscription(prefetch: .keepFull,
|
||||
whenFull: .customError(unreachable))
|
||||
}
|
||||
|
||||
func testFailWhileSendingValues() throws {
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .none,
|
||||
createSut: { $0.buffer(size: 5, prefetch: .byRequest, whenFull: .dropOldest) }
|
||||
)
|
||||
|
||||
helper.tracking.onValue = { _ in
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
}
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(1), .none)
|
||||
XCTAssertEqual(helper.publisher.send(2), .none)
|
||||
XCTAssertEqual(helper.publisher.send(3), .none)
|
||||
XCTAssertEqual(helper.publisher.send(4), .none)
|
||||
XCTAssertEqual(helper.publisher.send(5), .none)
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(3))
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Buffer"),
|
||||
.value(1),
|
||||
.completion(.failure(.oops)),
|
||||
.value(2),
|
||||
.value(3)])
|
||||
}
|
||||
|
||||
func testBufferByRequestDropNewestLifecycle() {
|
||||
testBufferLifecycle(prefetch: .byRequest,
|
||||
whenFull: .dropNewest)
|
||||
}
|
||||
|
||||
func testBufferByRequestDropOldestLifecycle() {
|
||||
testBufferLifecycle(prefetch: .byRequest,
|
||||
whenFull: .dropOldest)
|
||||
}
|
||||
|
||||
func testBufferByRequestCustomErrorLifecycle() {
|
||||
testBufferLifecycle(prefetch: .byRequest,
|
||||
whenFull: .customError { TestingError.oops })
|
||||
}
|
||||
|
||||
func testBufferKeepFullDropNewestLifecycle() {
|
||||
testBufferLifecycle(prefetch: .keepFull,
|
||||
whenFull: .dropNewest)
|
||||
}
|
||||
|
||||
func testBufferKeepFullDropOldestLifecycle() {
|
||||
testBufferLifecycle(prefetch: .keepFull,
|
||||
whenFull: .dropOldest)
|
||||
}
|
||||
|
||||
func testBufferKeepFullCustomErrorLifecycle() {
|
||||
testBufferLifecycle(prefetch: .keepFull,
|
||||
whenFull: .customError { TestingError.oops })
|
||||
}
|
||||
|
||||
func testBufferReflection() throws {
|
||||
try testReflection(
|
||||
parentInput: Int.self,
|
||||
parentFailure: TestingError.self,
|
||||
description: "Buffer",
|
||||
customMirror: expectedChildren(
|
||||
("values", "[]"),
|
||||
("state", .anything),
|
||||
("downstreamDemand", "max(0)"),
|
||||
("terminal", "nil")
|
||||
),
|
||||
playgroundDescription: "Buffer",
|
||||
{ $0.buffer(size: 13, prefetch: .keepFull, whenFull: .dropNewest) }
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Generic tests
|
||||
|
||||
private func testInitialDemand(
|
||||
withPrefetchStragety prefetch: Publishers.PrefetchStrategy,
|
||||
expectedSubscriptionHistory: [CustomSubscription.Event]
|
||||
) {
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: nil)
|
||||
let tracking = TrackingSubscriber()
|
||||
|
||||
subscription.onRequest = { _ in
|
||||
XCTAssertEqual(tracking.history, [])
|
||||
}
|
||||
|
||||
let buffer = publisher
|
||||
.buffer(size: 42, prefetch: prefetch, whenFull: .dropOldest)
|
||||
|
||||
buffer.subscribe(tracking)
|
||||
|
||||
XCTAssertEqual(tracking.history, [])
|
||||
|
||||
publisher.send(subscription: subscription)
|
||||
|
||||
XCTAssertEqual(subscription.history, expectedSubscriptionHistory)
|
||||
XCTAssertEqual(tracking.history, [.subscription("Buffer")])
|
||||
}
|
||||
|
||||
private func testBufferingInput(
|
||||
whenFull: Publishers.BufferingStrategy<TestingError>
|
||||
) throws {
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .none,
|
||||
createSut: { $0.buffer(size: 3, prefetch: .byRequest, whenFull: whenFull) }
|
||||
)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Buffer")])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(1), .none)
|
||||
XCTAssertEqual(helper.publisher.send(2), .none)
|
||||
XCTAssertEqual(helper.publisher.send(3), .none)
|
||||
XCTAssertEqual(helper.publisher.send(4), .none)
|
||||
XCTAssertEqual(helper.publisher.send(5), .none)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Buffer")])
|
||||
switch whenFull {
|
||||
case .dropNewest, .dropOldest:
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
case .customError:
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
|
||||
.cancelled])
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
@unknown default:
|
||||
unreachable()
|
||||
#endif
|
||||
}
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(3))
|
||||
|
||||
switch whenFull {
|
||||
case .dropNewest:
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Buffer"),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
|
||||
.requested(.max(3))])
|
||||
case .dropOldest:
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Buffer"),
|
||||
.value(3),
|
||||
.value(4),
|
||||
.value(5)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
|
||||
.requested(.max(3))])
|
||||
case .customError:
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Buffer"),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3),
|
||||
.completion(.failure(.oops))])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
|
||||
.cancelled,
|
||||
.requested(.max(3))])
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
@unknown default:
|
||||
unreachable()
|
||||
#endif
|
||||
}
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(1))
|
||||
|
||||
switch whenFull {
|
||||
case .dropNewest:
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Buffer"),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
|
||||
.requested(.max(3)),
|
||||
.requested(.max(1))])
|
||||
case .dropOldest:
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Buffer"),
|
||||
.value(3),
|
||||
.value(4),
|
||||
.value(5)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
|
||||
.requested(.max(3)),
|
||||
.requested(.max(1))])
|
||||
case .customError:
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Buffer"),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3),
|
||||
.completion(.failure(.oops))])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
|
||||
.cancelled,
|
||||
.requested(.max(3))])
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
@unknown default:
|
||||
unreachable()
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
private func testRecursion(
|
||||
prefetch: Publishers.PrefetchStrategy,
|
||||
whenFull: Publishers.BufferingStrategy<TestingError>
|
||||
) {
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .none,
|
||||
createSut: {
|
||||
$0.buffer(size: 5, prefetch: prefetch, whenFull: whenFull)
|
||||
}
|
||||
)
|
||||
|
||||
helper.tracking.onValue = { _ in
|
||||
helper.downstreamSubscription?.request(.max(1))
|
||||
helper.downstreamSubscription?.request(.none)
|
||||
}
|
||||
|
||||
helper.downstreamSubscription?.request(.max(3))
|
||||
|
||||
for i in 0 ..< 10 {
|
||||
switch prefetch {
|
||||
case .byRequest:
|
||||
XCTAssertEqual(helper.publisher.send(i), .none)
|
||||
case .keepFull:
|
||||
XCTAssertEqual(helper.publisher.send(i), .max(1))
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
@unknown default:
|
||||
unreachable()
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Buffer"),
|
||||
.value(0),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3),
|
||||
.value(4),
|
||||
.value(5),
|
||||
.value(6),
|
||||
.value(7),
|
||||
.value(8),
|
||||
.value(9)])
|
||||
|
||||
switch prefetch {
|
||||
case .byRequest:
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
|
||||
.requested(.max(3))])
|
||||
case .keepFull:
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(5)),
|
||||
.requested(.max(3))])
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
@unknown default:
|
||||
unreachable()
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
private func testRequestingFiniteDemand(
|
||||
prefetch: Publishers.PrefetchStrategy,
|
||||
whenFull: Publishers.BufferingStrategy<TestingError>
|
||||
) {
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: Subscribers.Demand.none,
|
||||
receiveValueDemand: .none,
|
||||
createSut: {
|
||||
$0.buffer(size: 5, prefetch: prefetch, whenFull: whenFull)
|
||||
}
|
||||
)
|
||||
|
||||
for i in 0 ..< 10 {
|
||||
XCTAssertEqual(helper.publisher.send(i), .none)
|
||||
}
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Buffer")])
|
||||
|
||||
switch (prefetch, whenFull) {
|
||||
case (.byRequest, .customError):
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
|
||||
.requested(.none),
|
||||
.cancelled])
|
||||
case (.byRequest, _):
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
|
||||
.requested(.none)])
|
||||
case (.keepFull, .customError):
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(5)),
|
||||
.requested(.none),
|
||||
.cancelled])
|
||||
case (.keepFull, _):
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(5)),
|
||||
.requested(.none)])
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
@unknown default:
|
||||
unreachable()
|
||||
#endif
|
||||
}
|
||||
|
||||
helper.downstreamSubscription?.request(.max(3))
|
||||
helper.downstreamSubscription?.request(.max(1))
|
||||
|
||||
switch whenFull {
|
||||
case .dropNewest:
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Buffer"),
|
||||
.value(0),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3)])
|
||||
case .dropOldest:
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Buffer"),
|
||||
.value(5),
|
||||
.value(6),
|
||||
.value(7),
|
||||
.value(8)])
|
||||
case .customError:
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Buffer"),
|
||||
.value(0),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.completion(.failure(.oops))])
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
@unknown default:
|
||||
unreachable()
|
||||
#endif
|
||||
}
|
||||
|
||||
switch (prefetch, whenFull) {
|
||||
case (.byRequest, .customError):
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
|
||||
.requested(.none),
|
||||
.cancelled,
|
||||
.requested(.max(3))])
|
||||
case (.byRequest, _):
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
|
||||
.requested(.none),
|
||||
.requested(.max(3)),
|
||||
.requested(.max(1))])
|
||||
case (.keepFull, .customError):
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(5)),
|
||||
.requested(.none),
|
||||
.cancelled,
|
||||
.requested(.max(6))])
|
||||
case (.keepFull, _):
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(5)),
|
||||
.requested(.none),
|
||||
.requested(.max(6)),
|
||||
.requested(.max(2))])
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
@unknown default:
|
||||
unreachable()
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
private func testRequestingUnlimitedDemand(
|
||||
prefetch: Publishers.PrefetchStrategy,
|
||||
whenFull: Publishers.BufferingStrategy<TestingError>
|
||||
) {
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .none,
|
||||
createSut: {
|
||||
$0.buffer(size: 5, prefetch: prefetch, whenFull: whenFull)
|
||||
}
|
||||
)
|
||||
|
||||
for i in 0 ..< 10 {
|
||||
switch prefetch {
|
||||
case .byRequest:
|
||||
XCTAssertEqual(helper.publisher.send(i), .none)
|
||||
case .keepFull:
|
||||
XCTAssertEqual(helper.publisher.send(i), .max(1))
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
@unknown default:
|
||||
unreachable()
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Buffer"),
|
||||
.value(0),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3),
|
||||
.value(4),
|
||||
.value(5),
|
||||
.value(6),
|
||||
.value(7),
|
||||
.value(8),
|
||||
.value(9)])
|
||||
|
||||
switch prefetch {
|
||||
case .byRequest:
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
|
||||
.requested(.unlimited)])
|
||||
case .keepFull:
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(5)),
|
||||
.requested(.unlimited)])
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
@unknown default:
|
||||
unreachable()
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
private func testReceiveValueAfterCompleting(
|
||||
_ completion: Subscribers.Completion<TestingError>
|
||||
) throws {
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .none,
|
||||
createSut: { $0.buffer(size: 3, prefetch: .byRequest, whenFull: .dropOldest) }
|
||||
)
|
||||
|
||||
helper.publisher.send(completion: completion)
|
||||
helper.publisher.send(completion: .finished) // Should be ignored
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
|
||||
switch completion {
|
||||
case .finished:
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Buffer")])
|
||||
case .failure:
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Buffer"),
|
||||
.completion(.failure(.oops))])
|
||||
}
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(1), .none)
|
||||
XCTAssertEqual(helper.publisher.send(2), .none)
|
||||
|
||||
switch completion {
|
||||
case .finished:
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Buffer")])
|
||||
case .failure:
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Buffer"),
|
||||
.completion(.failure(.oops))])
|
||||
}
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(2))
|
||||
|
||||
switch completion {
|
||||
case .finished:
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Buffer"),
|
||||
.value(1),
|
||||
.value(2)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
|
||||
.requested(.max(2))])
|
||||
case .failure:
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Buffer"),
|
||||
.completion(completion)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
}
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(1))
|
||||
|
||||
switch completion {
|
||||
case .finished:
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Buffer"),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.completion(completion)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
|
||||
.requested(.max(2)),
|
||||
.requested(.max(1))])
|
||||
case .failure:
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Buffer"),
|
||||
.completion(completion)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
}
|
||||
}
|
||||
|
||||
private func testBufferLifecycle(
|
||||
prefetch: Publishers.PrefetchStrategy,
|
||||
whenFull: Publishers.BufferingStrategy<TestingError>
|
||||
) {
|
||||
var deinitCounter = 0
|
||||
let onDeinit = { deinitCounter += 1 }
|
||||
|
||||
do {
|
||||
let publisher = CustomPublisher(subscription: CustomSubscription())
|
||||
publisher.onDeinit = onDeinit
|
||||
let buffer = publisher.buffer(size: 1, prefetch: prefetch, whenFull: whenFull)
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: { $0.request(.unlimited) },
|
||||
onDeinit: onDeinit
|
||||
)
|
||||
buffer.subscribe(tracking)
|
||||
publisher.send(completion: .finished)
|
||||
}
|
||||
|
||||
XCTAssertEqual(deinitCounter, 2)
|
||||
|
||||
do {
|
||||
let publisher = CustomPublisher(subscription: CustomSubscription())
|
||||
publisher.onDeinit = onDeinit
|
||||
let buffer = publisher.buffer(size: 1, prefetch: prefetch, whenFull: whenFull)
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: { $0.request(.unlimited) },
|
||||
onDeinit: onDeinit
|
||||
)
|
||||
buffer.subscribe(tracking)
|
||||
publisher.send(completion: .failure(.oops))
|
||||
}
|
||||
|
||||
XCTAssertEqual(deinitCounter, 4)
|
||||
|
||||
var downstreamSubscription: Subscription?
|
||||
|
||||
do {
|
||||
let publisher = CustomPublisher(subscription: CustomSubscription())
|
||||
publisher.onDeinit = onDeinit
|
||||
let buffer = publisher.buffer(size: 1, prefetch: prefetch, whenFull: whenFull)
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: { downstreamSubscription = $0 },
|
||||
onDeinit: onDeinit
|
||||
)
|
||||
buffer.subscribe(tracking)
|
||||
publisher.send(completion: .failure(.oops))
|
||||
}
|
||||
|
||||
XCTAssertEqual(deinitCounter, 6)
|
||||
downstreamSubscription?.cancel()
|
||||
XCTAssertEqual(deinitCounter, 6)
|
||||
|
||||
do {
|
||||
let publisher = CustomPublisher(subscription: CustomSubscription())
|
||||
publisher.onDeinit = onDeinit
|
||||
let buffer = publisher.buffer(size: 1, prefetch: prefetch, whenFull: whenFull)
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: { $0.request(.unlimited) },
|
||||
onDeinit: onDeinit
|
||||
)
|
||||
buffer.subscribe(tracking)
|
||||
}
|
||||
|
||||
XCTAssertEqual(deinitCounter, 6)
|
||||
}
|
||||
|
||||
private func testBufferReceiveSubscriptionTwice(
|
||||
prefetch: Publishers.PrefetchStrategy,
|
||||
whenFull: Publishers.BufferingStrategy<TestingError>
|
||||
) throws {
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .none,
|
||||
createSut: { $0.buffer(size: 2, prefetch: prefetch, whenFull: whenFull) }
|
||||
)
|
||||
|
||||
switch prefetch {
|
||||
case .keepFull:
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(2))])
|
||||
case .byRequest:
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
@unknown default:
|
||||
unreachable()
|
||||
#endif
|
||||
}
|
||||
|
||||
let secondSubscription = CustomSubscription()
|
||||
|
||||
try XCTUnwrap(helper.publisher.subscriber)
|
||||
.receive(subscription: secondSubscription)
|
||||
|
||||
XCTAssertEqual(secondSubscription.history, [.cancelled])
|
||||
|
||||
try XCTUnwrap(helper.publisher.subscriber)
|
||||
.receive(subscription: helper.subscription)
|
||||
|
||||
switch prefetch {
|
||||
case .keepFull:
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(2)),
|
||||
.cancelled])
|
||||
case .byRequest:
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
|
||||
.cancelled])
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
@unknown default:
|
||||
unreachable()
|
||||
#endif
|
||||
}
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
|
||||
switch prefetch {
|
||||
case .keepFull:
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(2)),
|
||||
.cancelled,
|
||||
.cancelled])
|
||||
case .byRequest:
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
|
||||
.cancelled,
|
||||
.cancelled])
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
@unknown default:
|
||||
unreachable()
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
private func testBufferReceiveValueBeforeSubscription(
|
||||
prefetch: Publishers.PrefetchStrategy,
|
||||
whenFull: Publishers.BufferingStrategy<Never>
|
||||
) {
|
||||
testReceiveValueBeforeSubscription(
|
||||
value: 0,
|
||||
expected: .history([], demand: .none),
|
||||
{ $0.buffer(size: 2, prefetch: prefetch, whenFull: whenFull) }
|
||||
)
|
||||
}
|
||||
|
||||
private func testBufferReceiveCompletionBeforeSubscription(
|
||||
prefetch: Publishers.PrefetchStrategy,
|
||||
whenFull: Publishers.BufferingStrategy<Never>
|
||||
) {
|
||||
testReceiveCompletionBeforeSubscription(
|
||||
inputType: Int.self,
|
||||
expected: .history([]),
|
||||
{ $0.buffer(size: 2, prefetch: prefetch, whenFull: whenFull) }
|
||||
)
|
||||
}
|
||||
|
||||
private func testBufferRequestBeforeSubscription(
|
||||
prefetch: Publishers.PrefetchStrategy,
|
||||
whenFull: Publishers.BufferingStrategy<Never>
|
||||
) {
|
||||
testRequestBeforeSubscription(
|
||||
inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
{ $0.buffer(size: 2, prefetch: prefetch, whenFull: whenFull) }
|
||||
)
|
||||
}
|
||||
|
||||
private func testBufferCancelBeforeSubscription(
|
||||
prefetch: Publishers.PrefetchStrategy,
|
||||
whenFull: Publishers.BufferingStrategy<Never>
|
||||
) {
|
||||
testCancelBeforeSubscription(
|
||||
inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
{ $0.buffer(size: 2, prefetch: prefetch, whenFull: whenFull) }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,622 @@
|
||||
//
|
||||
// CatchTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 25.12.2019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class CatchTests: XCTestCase {
|
||||
|
||||
// MARK: - Catch
|
||||
|
||||
func testSimpleCatch() {
|
||||
CatchTests
|
||||
.testWithSequence(expectedSubscription: "Catch") { upstream, new in
|
||||
upstream.catch { _ in new }
|
||||
}
|
||||
}
|
||||
|
||||
func testCatchReceiveSubscriptionTwice() throws {
|
||||
try testReceiveSubscriptionTwice { $0.catch(Fail.init) }
|
||||
try testReceiveSubscriptionTwice { publisher in
|
||||
Fail(outputType: Int.self, failure: TestingError.oops)
|
||||
.catch { _ in publisher }
|
||||
}
|
||||
}
|
||||
|
||||
func testCatchCrashesOnUnwantedInput() {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .none,
|
||||
createSut: { $0.catch { _ in Just(-1) } })
|
||||
assertCrashes {
|
||||
_ = helper.publisher.send(42)
|
||||
}
|
||||
}
|
||||
|
||||
func testCatchPreservesDemand() throws {
|
||||
try CatchTests.testPreservesDemand(expectedSubscription: "Catch") {
|
||||
$0.catch($1)
|
||||
}
|
||||
}
|
||||
|
||||
func testCatchUpstreamFinishes() {
|
||||
CatchTests.testUpstreamFinishes(expectedSubscription: "Catch") {
|
||||
$0.catch($1)
|
||||
}
|
||||
}
|
||||
|
||||
func testCatchRecursion() {
|
||||
testRecursion(expectedSubscription: "Catch") {
|
||||
$0.catch($1)
|
||||
}
|
||||
}
|
||||
|
||||
func testCatchReceiveValueBeforeSubscription() {
|
||||
testReceiveValueBeforeSubscription(value: 1, expected: .crash) {
|
||||
$0.catch { _ in Just(13) }
|
||||
}
|
||||
}
|
||||
|
||||
func testCatchReceiveCompletionBeforeSubscription() {
|
||||
testReceiveCompletionBeforeSubscription(
|
||||
inputType: Int.self,
|
||||
expected: .history([]),
|
||||
{ $0.catch { _ in Just(13) } }
|
||||
)
|
||||
}
|
||||
|
||||
func testCatchRequestPendingPost() {
|
||||
CatchTests.testRequestPendingPost(expectedSubscription: "Catch",
|
||||
{ $0.catch($1) })
|
||||
}
|
||||
|
||||
func testCatchCancelPendingPost() {
|
||||
CatchTests.testCancelPendingPost(expectedSubscription: "Catch",
|
||||
{ $0.catch($1) })
|
||||
}
|
||||
|
||||
func testCatchRequestPost() throws {
|
||||
try CatchTests.testRequestPost(expectedSubscription: "Catch",
|
||||
{ $0.catch($1) })
|
||||
}
|
||||
|
||||
func testCatchCancellationBeforeRecovering() throws {
|
||||
try CatchTests.testCancellationBeforeRecovering(expectedSubscription: "Catch",
|
||||
{ $0.catch($1) })
|
||||
}
|
||||
|
||||
func testCatchCancellationAfterRecovering() throws {
|
||||
try CatchTests.testCancellationAfterRecovering(expectedSubscription: "Catch",
|
||||
{ $0.catch($1) })
|
||||
}
|
||||
|
||||
func testCatchUpstreamFailsTwice() {
|
||||
testUpstreamFailsTwice(expectedSubscription: "Catch") { $0.catch($1) }
|
||||
}
|
||||
|
||||
func testCatchReflection() throws {
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: TestingError.self,
|
||||
description: "Catch",
|
||||
customMirror: expectedChildren(
|
||||
("downstream", .contains("TrackingSubscriberBase")),
|
||||
("demand", "max(0)")
|
||||
),
|
||||
playgroundDescription: "Catch",
|
||||
{ $0.catch(Fail.init) })
|
||||
|
||||
try testReflection(
|
||||
parentInput: Int.self,
|
||||
parentFailure: TestingError.self,
|
||||
description: "Catch",
|
||||
customMirror: expectedChildren(
|
||||
("downstream", .contains("TrackingSubscriberBase")),
|
||||
("demand", "max(0)")
|
||||
),
|
||||
playgroundDescription: "Catch",
|
||||
{ publisher in
|
||||
Fail(outputType: Int.self, failure: TestingError.oops)
|
||||
.catch { _ in publisher }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - TryCatch
|
||||
|
||||
func testSimpleTryCatch() {
|
||||
CatchTests
|
||||
.testWithSequence(expectedSubscription: "TryCatch") { upstream, new in
|
||||
upstream.tryCatch { _ in new }
|
||||
}
|
||||
}
|
||||
|
||||
func testTryCatchReceiveSubscriptionTwice() throws {
|
||||
|
||||
try testReceiveSubscriptionTwice { $0.tryCatch(Fail.init) }
|
||||
|
||||
try testReceiveSubscriptionTwice { publisher in
|
||||
Fail(outputType: Int.self, failure: TestingError.oops)
|
||||
.tryCatch { _ in publisher }
|
||||
}
|
||||
}
|
||||
|
||||
func testTryCatchCrashesOnUnwantedInput() {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .none,
|
||||
createSut: { $0.tryCatch { _ in Just(-1) } })
|
||||
assertCrashes {
|
||||
_ = helper.publisher.send(42)
|
||||
}
|
||||
}
|
||||
|
||||
func testTryCatchPreservesDemand() throws {
|
||||
try CatchTests.testPreservesDemand(expectedSubscription: "TryCatch") {
|
||||
$0.tryCatch($1)
|
||||
}
|
||||
}
|
||||
|
||||
func testTryCatchUpstreamFinishes() {
|
||||
CatchTests.testUpstreamFinishes(expectedSubscription: "TryCatch") {
|
||||
$0.tryCatch($1)
|
||||
}
|
||||
}
|
||||
|
||||
func testTryCatchHandlerThrows() {
|
||||
|
||||
var handledErrors = [TestingError]()
|
||||
|
||||
func handler(_ error: TestingError) throws -> Just<Int> {
|
||||
handledErrors.append(error)
|
||||
throw "oops2" as TestingError
|
||||
}
|
||||
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .none,
|
||||
createSut: { $0.tryCatch(handler) })
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(1), .none)
|
||||
XCTAssertEqual(helper.publisher.send(2), .none)
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
|
||||
XCTAssertEqual(helper.tracking.history,
|
||||
[.subscription("TryCatch"),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.completion(.failure("oops2" as TestingError))])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(handledErrors, [.oops])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(-1), .none)
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
helper.publisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history,
|
||||
[.subscription("TryCatch"),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.completion(.failure("oops2" as TestingError)),
|
||||
.value(-1)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(handledErrors, [.oops])
|
||||
}
|
||||
|
||||
func testTryCatchRecursion() {
|
||||
testRecursion(expectedSubscription: "TryCatch") {
|
||||
$0.tryCatch($1)
|
||||
}
|
||||
}
|
||||
|
||||
func testTryCatchReceiveValueBeforeSubscription() {
|
||||
testReceiveValueBeforeSubscription(value: 1, expected: .crash) {
|
||||
$0.tryCatch { _ in Just(13) }
|
||||
}
|
||||
}
|
||||
|
||||
func testTryCatchReceiveCompletionBeforeSubscription() {
|
||||
testReceiveCompletionBeforeSubscription(
|
||||
inputType: Int.self,
|
||||
expected: .history([]),
|
||||
{ $0.tryCatch { _ in Just(13) } }
|
||||
)
|
||||
}
|
||||
|
||||
func testTryCatchRequestPendingPost() {
|
||||
CatchTests.testRequestPendingPost(expectedSubscription: "TryCatch",
|
||||
{ $0.tryCatch($1) })
|
||||
}
|
||||
|
||||
func testTryCatchCancelPendingPost() {
|
||||
CatchTests.testCancelPendingPost(expectedSubscription: "TryCatch",
|
||||
{ $0.tryCatch($1) })
|
||||
}
|
||||
|
||||
func testTryCatchRequestPost() throws {
|
||||
try CatchTests.testRequestPost(expectedSubscription: "TryCatch",
|
||||
{ $0.tryCatch($1) })
|
||||
}
|
||||
|
||||
func testTryCatchCancellationBeforeRecovering() throws {
|
||||
try CatchTests.testCancellationBeforeRecovering(expectedSubscription: "TryCatch",
|
||||
{ $0.tryCatch($1) })
|
||||
}
|
||||
|
||||
func testTryCatchCancellationAfterRecovering() throws {
|
||||
try CatchTests.testCancellationAfterRecovering(expectedSubscription: "TryCatch",
|
||||
{ $0.tryCatch($1) })
|
||||
}
|
||||
|
||||
func testTryCatchUpstreamFailsTwice() {
|
||||
testUpstreamFailsTwice(expectedSubscription: "TryCatch") { $0.tryCatch($1) }
|
||||
}
|
||||
|
||||
func testTryCatchReflection() throws {
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: TestingError.self,
|
||||
description: "TryCatch",
|
||||
customMirror: expectedChildren(
|
||||
("downstream", .contains("TrackingSubscriberBase")),
|
||||
("demand", "max(0)")
|
||||
),
|
||||
playgroundDescription: "TryCatch",
|
||||
{ $0.tryCatch(Fail.init) })
|
||||
|
||||
try testReflection(
|
||||
parentInput: Int.self,
|
||||
parentFailure: TestingError.self,
|
||||
description: "TryCatch",
|
||||
customMirror: expectedChildren(
|
||||
("downstream", .contains("TrackingSubscriberBase")),
|
||||
("demand", "max(0)")
|
||||
),
|
||||
playgroundDescription: "TryCatch",
|
||||
{ publisher in
|
||||
Fail<Int, TestingError>(error: .oops).tryCatch { _ in publisher }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Generic tests
|
||||
|
||||
private typealias TestSequence = Publishers.Sequence<[Int], Never>
|
||||
|
||||
private static func testWithSequence<Operator: Publisher>(
|
||||
expectedSubscription: StringSubscription,
|
||||
_ makeCatch: (Publishers.TryMap<TestSequence, Int>, TestSequence) -> Operator
|
||||
) where Operator.Output == Int {
|
||||
let throwingSequence = TestSequence(sequence: Array(0 ..< 10))
|
||||
.tryMap { v -> Int in
|
||||
if v < 5 {
|
||||
return v
|
||||
} else {
|
||||
throw TestingError.oops
|
||||
}
|
||||
}
|
||||
|
||||
let `catch` = makeCatch(throwingSequence, [3, 2, 1, 0].publisher)
|
||||
|
||||
let tracking = TrackingSubscriberBase<Int, Operator.Failure>(
|
||||
receiveSubscription: { $0.request(.max(1)) },
|
||||
receiveValue: { _ in .max(1) }
|
||||
)
|
||||
`catch`.subscribe(tracking)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription(expectedSubscription),
|
||||
.value(0),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3),
|
||||
.value(4),
|
||||
.value(3),
|
||||
.value(2),
|
||||
.value(1),
|
||||
.value(0),
|
||||
.completion(.finished)])
|
||||
}
|
||||
|
||||
private static func testPreservesDemand<Operator: Publisher>(
|
||||
expectedSubscription: StringSubscription,
|
||||
_ makeCatch:
|
||||
(CustomPublisher,
|
||||
@escaping (TestingError) -> CustomPublisherBase<Int, Error>) -> Operator
|
||||
) throws where Operator.Output == Int {
|
||||
|
||||
let errorHandlerSubscription = CustomSubscription()
|
||||
let errorHandlerPublisher = CustomPublisherBase<Int, Error>(
|
||||
subscription: errorHandlerSubscription
|
||||
)
|
||||
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(12),
|
||||
receiveValueDemand: .max(3),
|
||||
createSut: { makeCatch($0) { _ in errorHandlerPublisher } }
|
||||
)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(12))])
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(5))
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(12)),
|
||||
.requested(.max(5))])
|
||||
|
||||
for i in 1 ... 8 {
|
||||
XCTAssertEqual(helper.publisher.send(i), .max(3))
|
||||
}
|
||||
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3),
|
||||
.value(4),
|
||||
.value(5),
|
||||
.value(6),
|
||||
.value(7),
|
||||
.value(8)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(12)),
|
||||
.requested(.max(5))])
|
||||
XCTAssertEqual(errorHandlerSubscription.history, [.requested(.max(33))])
|
||||
}
|
||||
|
||||
private static func testUpstreamFinishes<Operator: Publisher>(
|
||||
expectedSubscription: StringSubscription,
|
||||
_ makeCatch: (CustomPublisher,
|
||||
@escaping (TestingError) -> Fail<Int, Error>) -> Operator
|
||||
) where Operator.Output == Int {
|
||||
var counter = 0
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(2),
|
||||
receiveValueDemand: .none,
|
||||
createSut: { makeCatch($0) { counter += 1; return Fail(error: $0) } }
|
||||
)
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(1), .none)
|
||||
XCTAssertEqual(helper.publisher.send(2), .none)
|
||||
helper.publisher.send(completion: .finished)
|
||||
helper.publisher.send(completion: .finished)
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.completion(.finished)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(2))])
|
||||
XCTAssertEqual(counter, 0)
|
||||
}
|
||||
|
||||
private func testRecursion<Operator: Publisher>(
|
||||
expectedSubscription: StringSubscription,
|
||||
_ makeCatch: @escaping (CustomPublisher,
|
||||
@escaping (TestingError) -> Just<Int>) -> Operator
|
||||
) where Operator.Output == Int {
|
||||
|
||||
func createSut(_ publisher: CustomPublisher) -> Operator {
|
||||
return makeCatch(publisher) { _ in
|
||||
publisher.send(completion: .failure(.oops))
|
||||
return Just(13)
|
||||
}
|
||||
}
|
||||
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .none,
|
||||
createSut: createSut
|
||||
)
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(1), .none)
|
||||
XCTAssertEqual(helper.publisher.send(2), .none)
|
||||
|
||||
assertCrashes {
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
}
|
||||
}
|
||||
|
||||
private static func testRequestPendingPost<Operator: Publisher>(
|
||||
expectedSubscription: StringSubscription,
|
||||
_ makeCatch: (CustomPublisher,
|
||||
@escaping (TestingError) -> CustomPublisher) -> Operator
|
||||
) where Operator.Output == Int {
|
||||
|
||||
let handlerSubscription = CustomSubscription()
|
||||
let handlerPublisher = CustomPublisher(subscription: handlerSubscription)
|
||||
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(3),
|
||||
receiveValueDemand: .none,
|
||||
createSut: { makeCatch($0) { _ in handlerPublisher } }
|
||||
)
|
||||
|
||||
handlerPublisher.willSubscribe = { _ in
|
||||
guard let downstreamSubscription = helper.downstreamSubscription else {
|
||||
XCTFail("missing downstream subscription")
|
||||
return
|
||||
}
|
||||
downstreamSubscription.request(.max(10))
|
||||
XCTAssertEqual(handlerSubscription.history, [])
|
||||
}
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(1), .none)
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription),
|
||||
.value(1)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(3))])
|
||||
XCTAssertEqual(handlerSubscription.history, [.requested(.max(12))])
|
||||
}
|
||||
|
||||
private static func testCancelPendingPost<Operator: Publisher>(
|
||||
expectedSubscription: StringSubscription,
|
||||
_ makeCatch: (CustomPublisher,
|
||||
@escaping (TestingError) -> CustomPublisher) -> Operator
|
||||
) where Operator.Output == Int {
|
||||
|
||||
let handlerSubscription = CustomSubscription()
|
||||
let handlerPublisher = CustomPublisher(subscription: handlerSubscription)
|
||||
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(3),
|
||||
receiveValueDemand: .none,
|
||||
createSut: { makeCatch($0) { _ in handlerPublisher } }
|
||||
)
|
||||
|
||||
handlerPublisher.willSubscribe = { _ in
|
||||
guard let downstreamSubscription = helper.downstreamSubscription else {
|
||||
XCTFail("missing downstream subscription")
|
||||
return
|
||||
}
|
||||
downstreamSubscription.cancel()
|
||||
XCTAssertEqual(handlerSubscription.history, [])
|
||||
}
|
||||
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(3))])
|
||||
XCTAssertEqual(handlerSubscription.history, [.requested(.max(3))])
|
||||
|
||||
handlerPublisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription),
|
||||
.completion(.finished)])
|
||||
XCTAssertEqual(handlerSubscription.history, [.requested(.max(3))])
|
||||
}
|
||||
|
||||
private static func testRequestPost<Operator: Publisher>(
|
||||
expectedSubscription: StringSubscription,
|
||||
_ makeCatch: (CustomPublisher,
|
||||
@escaping (TestingError) -> CustomPublisher) -> Operator
|
||||
) throws where Operator.Output == Int {
|
||||
|
||||
let handlerSubscription = CustomSubscription()
|
||||
let handlerPublisher = CustomPublisher(subscription: handlerSubscription)
|
||||
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .none,
|
||||
createSut: { makeCatch($0) { _ in handlerPublisher } }
|
||||
)
|
||||
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription)])
|
||||
XCTAssertEqual(helper.subscription.history, [])
|
||||
XCTAssertNotNil(handlerPublisher.subscriber)
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(12))
|
||||
XCTAssertEqual(handlerSubscription.history, [.requested(.max(12))])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription)])
|
||||
|
||||
XCTAssertEqual(handlerPublisher.send(100), .none)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription),
|
||||
.value(100)])
|
||||
}
|
||||
|
||||
private static func testCancellationBeforeRecovering<Operator: Publisher>(
|
||||
expectedSubscription: StringSubscription,
|
||||
_ makeCatch: (CustomPublisher,
|
||||
@escaping (TestingError) -> CustomPublisher) -> Operator
|
||||
) throws where Operator.Output == Int {
|
||||
|
||||
func handler(_ error: TestingError) -> CustomPublisher {
|
||||
XCTFail("Should not be called")
|
||||
return CustomPublisher(subscription: CustomSubscription())
|
||||
}
|
||||
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(1),
|
||||
receiveValueDemand: .max(3),
|
||||
createSut: { makeCatch($0, handler) }
|
||||
)
|
||||
|
||||
let downstreamSubscription = try XCTUnwrap(helper.downstreamSubscription)
|
||||
|
||||
downstreamSubscription.cancel()
|
||||
downstreamSubscription.request(.max(199))
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(1), .max(3))
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(1)), .cancelled])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription),
|
||||
.value(1)])
|
||||
}
|
||||
|
||||
private static func testCancellationAfterRecovering<Operator: Publisher>(
|
||||
expectedSubscription: StringSubscription,
|
||||
_ makeCatch: (CustomPublisher,
|
||||
@escaping (TestingError) -> CustomPublisher) -> Operator
|
||||
) throws where Operator.Output == Int {
|
||||
|
||||
let handlerSubscription = CustomSubscription()
|
||||
let handlerPublisher = CustomPublisher(subscription: handlerSubscription)
|
||||
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .max(3),
|
||||
createSut: { makeCatch($0) { _ in handlerPublisher } }
|
||||
)
|
||||
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
|
||||
let downstreamSubscription = try XCTUnwrap(helper.downstreamSubscription)
|
||||
downstreamSubscription.cancel()
|
||||
|
||||
XCTAssertEqual(handlerPublisher.send(1), .max(3))
|
||||
handlerPublisher.send(completion: .finished)
|
||||
handlerPublisher.send(completion: .failure(.oops))
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription),
|
||||
.value(1)])
|
||||
XCTAssertEqual(helper.subscription.history, [])
|
||||
XCTAssertEqual(handlerSubscription.history, [.cancelled])
|
||||
|
||||
let extraSubscription = CustomSubscription()
|
||||
handlerPublisher.send(subscription: extraSubscription)
|
||||
XCTAssertEqual(extraSubscription.history, [.cancelled])
|
||||
}
|
||||
|
||||
private func testUpstreamFailsTwice<Operator: Publisher>(
|
||||
expectedSubscription: StringSubscription,
|
||||
_ makeCatch: (CustomPublisher,
|
||||
@escaping (TestingError) -> CustomPublisher) -> Operator
|
||||
) where Operator.Output == Int {
|
||||
|
||||
let handlerSubscription = CustomSubscription()
|
||||
let handlerPublisher = CustomPublisher(subscription: handlerSubscription)
|
||||
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .max(3),
|
||||
createSut: { makeCatch($0) { _ in handlerPublisher } }
|
||||
)
|
||||
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
|
||||
assertCrashes {
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
//
|
||||
// CollectByCountTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 25.12.2019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class CollectByCountTests: XCTestCase {
|
||||
|
||||
func testBasicBehavior() {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(2),
|
||||
receiveValueDemand: .max(3),
|
||||
createSut: { $0.collect(4) })
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(8))])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("CollectByCount")])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(1), .none)
|
||||
XCTAssertEqual(helper.publisher.send(2), .none)
|
||||
XCTAssertEqual(helper.publisher.send(3), .none)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("CollectByCount")])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(4), .max(12))
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("CollectByCount"),
|
||||
.value([1, 2, 3, 4])])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(5), .none)
|
||||
XCTAssertEqual(helper.publisher.send(6), .none)
|
||||
XCTAssertEqual(helper.publisher.send(7), .none)
|
||||
XCTAssertEqual(helper.publisher.send(8), .max(12))
|
||||
XCTAssertEqual(helper.publisher.send(9), .none)
|
||||
XCTAssertEqual(helper.publisher.send(10), .none)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("CollectByCount"),
|
||||
.value([1, 2, 3, 4]),
|
||||
.value([5, 6, 7, 8])])
|
||||
|
||||
helper.publisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("CollectByCount"),
|
||||
.value([1, 2, 3, 4]),
|
||||
.value([5, 6, 7, 8]),
|
||||
.value([9, 10]),
|
||||
.completion(.finished)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(8))])
|
||||
}
|
||||
|
||||
func testFinishWithEmptyBuffer() throws {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .max(3),
|
||||
createSut: { $0.collect(4) })
|
||||
XCTAssertEqual(helper.publisher.send(1), .none)
|
||||
XCTAssertEqual(helper.publisher.send(2), .none)
|
||||
XCTAssertEqual(helper.publisher.send(3), .none)
|
||||
XCTAssertEqual(helper.publisher.send(4), .max(12))
|
||||
helper.publisher.send(completion: .finished)
|
||||
helper.publisher.send(completion: .finished)
|
||||
XCTAssertEqual(helper.publisher.send(5), .none)
|
||||
XCTAssertEqual(helper.publisher.send(6), .none)
|
||||
XCTAssertEqual(helper.publisher.send(7), .none)
|
||||
XCTAssertEqual(helper.publisher.send(8), .none)
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(2))
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("CollectByCount"),
|
||||
.value([1, 2, 3, 4]),
|
||||
.completion(.finished),
|
||||
.completion(.finished)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
}
|
||||
|
||||
func testFailureWithEmptyBuffer() throws {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .max(3),
|
||||
createSut: { $0.collect(4) })
|
||||
XCTAssertEqual(helper.publisher.send(1), .none)
|
||||
XCTAssertEqual(helper.publisher.send(2), .none)
|
||||
XCTAssertEqual(helper.publisher.send(3), .none)
|
||||
XCTAssertEqual(helper.publisher.send(4), .max(12))
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
XCTAssertEqual(helper.publisher.send(5), .none)
|
||||
XCTAssertEqual(helper.publisher.send(6), .none)
|
||||
XCTAssertEqual(helper.publisher.send(7), .none)
|
||||
XCTAssertEqual(helper.publisher.send(8), .none)
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(2))
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("CollectByCount"),
|
||||
.value([1, 2, 3, 4]),
|
||||
.completion(.failure(.oops)),
|
||||
.completion(.failure(.oops))])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
}
|
||||
|
||||
func testFailureWithNonEmptyBuffer() throws {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .max(3),
|
||||
createSut: { $0.collect(4) })
|
||||
XCTAssertEqual(helper.publisher.send(1), .none)
|
||||
XCTAssertEqual(helper.publisher.send(2), .none)
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
XCTAssertEqual(helper.publisher.send(5), .none)
|
||||
XCTAssertEqual(helper.publisher.send(6), .none)
|
||||
XCTAssertEqual(helper.publisher.send(7), .none)
|
||||
XCTAssertEqual(helper.publisher.send(8), .none)
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(2))
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("CollectByCount"),
|
||||
.completion(.failure(.oops)),
|
||||
.completion(.failure(.oops))])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
}
|
||||
|
||||
func testCrashesOnZeroDemand() throws {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .max(3),
|
||||
createSut: { $0.collect(4) })
|
||||
|
||||
try assertCrashes {
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.none)
|
||||
}
|
||||
}
|
||||
|
||||
func testCancelThenFinish() throws {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .max(3),
|
||||
createSut: { $0.collect(4) })
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(1), .none)
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
helper.publisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("CollectByCount"),
|
||||
.completion(.finished)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
|
||||
.cancelled])
|
||||
}
|
||||
|
||||
func testCancelAlreadyCancelled() throws {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .max(3),
|
||||
createSut: { $0.collect(4) })
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
|
||||
let subscription2 = CustomSubscription()
|
||||
helper.publisher.send(subscription: subscription2)
|
||||
XCTAssertEqual(subscription2.history, [.cancelled])
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(2))
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
helper.publisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("CollectByCount"),
|
||||
.completion(.finished)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
|
||||
.cancelled])
|
||||
}
|
||||
|
||||
func testCollectByCountReceiveValueBeforeSubscription() {
|
||||
testReceiveValueBeforeSubscription(value: 42,
|
||||
expected: .history([], demand: .none),
|
||||
{ $0.collect(19) })
|
||||
}
|
||||
|
||||
func testCollectByCountReceiveCompletionBeforeSubscription() {
|
||||
testReceiveCompletionBeforeSubscription(
|
||||
inputType: Int.self,
|
||||
expected: .history([.completion(.finished)]),
|
||||
{ $0.collect(19) }
|
||||
)
|
||||
}
|
||||
|
||||
func testCollectByCountRequestBeforeSubscription() {
|
||||
testRequestBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
{ $0.collect(19) })
|
||||
}
|
||||
|
||||
func testCollectByCountCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
{ $0.collect(19) })
|
||||
}
|
||||
|
||||
func testCollectByCountReceiveSubscriptionTwice() throws {
|
||||
try testReceiveSubscriptionTwice { $0.collect(19) }
|
||||
}
|
||||
|
||||
func testCollectByCountLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
{ $0.collect(42) })
|
||||
}
|
||||
|
||||
func testCollectByCountReflection() throws {
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: Error.self,
|
||||
description: "CollectByCount",
|
||||
customMirror: expectedChildren(
|
||||
("downstream", .contains("TrackingSubscriberBase")),
|
||||
("upstreamSubscription", .anything),
|
||||
("buffer", "[]"),
|
||||
("count", "53")
|
||||
),
|
||||
playgroundDescription: "CollectByCount",
|
||||
{ $0.collect(53) })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,452 @@
|
||||
//
|
||||
// ConcatenateTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 09.12.2019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class ConcatenateTests: XCTestCase {
|
||||
|
||||
func testAppendBasicBehavior() {
|
||||
let subscription1 = CustomSubscription()
|
||||
let subscription2 = CustomSubscription()
|
||||
let publisher1 = CustomPublisher(subscription: subscription1)
|
||||
let publisher2 = CustomPublisher(subscription: subscription2)
|
||||
|
||||
let append = publisher1.append(publisher2)
|
||||
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: { $0.request(.max(10)) },
|
||||
receiveValue: { .max($0) },
|
||||
receiveCompletion: { _ in }
|
||||
)
|
||||
|
||||
append.subscribe(tracking)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Concatenate")])
|
||||
XCTAssertEqual(subscription1.history, [.requested(.max(10))])
|
||||
XCTAssertEqual(subscription2.history, [])
|
||||
|
||||
XCTAssertEqual(publisher1.send(1), .max(1))
|
||||
XCTAssertEqual(publisher2.send(-1), .none)
|
||||
XCTAssertEqual(publisher1.send(2), .max(2))
|
||||
XCTAssertEqual(publisher2.send(-2), .none)
|
||||
XCTAssertEqual(publisher1.send(3), .max(3))
|
||||
XCTAssertEqual(publisher2.send(-3), .none)
|
||||
publisher2.send(completion: .failure(.oops))
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Concatenate"),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3)])
|
||||
XCTAssertEqual(subscription1.history, [.requested(.max(10))])
|
||||
XCTAssertEqual(subscription2.history, [])
|
||||
|
||||
publisher1.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Concatenate"),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3)])
|
||||
XCTAssertEqual(subscription1.history, [.requested(.max(10))])
|
||||
XCTAssertEqual(subscription2.history, [.requested(.max(13))])
|
||||
|
||||
XCTAssertEqual(publisher1.send(4000), .max(4000))
|
||||
XCTAssertEqual(publisher2.send(5), .max(5))
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Concatenate"),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3),
|
||||
.value(4000),
|
||||
.value(5)])
|
||||
XCTAssertEqual(subscription1.history, [.requested(.max(10))])
|
||||
XCTAssertEqual(subscription2.history, [.requested(.max(13))])
|
||||
|
||||
publisher2.send(completion: .finished)
|
||||
publisher2.send(completion: .finished)
|
||||
publisher1.send(completion: .finished)
|
||||
publisher1.send(completion: .failure(.oops))
|
||||
publisher2.send(completion: .failure(.oops))
|
||||
|
||||
XCTAssertEqual(publisher1.send(6000), .max(6000))
|
||||
XCTAssertEqual(publisher2.send(7), .max(7))
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Concatenate"),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3),
|
||||
.value(4000),
|
||||
.value(5),
|
||||
.completion(.finished),
|
||||
.completion(.finished),
|
||||
.completion(.finished),
|
||||
.completion(.failure(.oops)),
|
||||
.completion(.failure(.oops)),
|
||||
.value(6000),
|
||||
.value(7)])
|
||||
XCTAssertEqual(subscription1.history, [.requested(.max(10))])
|
||||
XCTAssertEqual(subscription2.history, [.requested(.max(13))])
|
||||
|
||||
let subscription3 = CustomSubscription()
|
||||
publisher2.send(subscription: subscription3)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Concatenate"),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3),
|
||||
.value(4000),
|
||||
.value(5),
|
||||
.completion(.finished),
|
||||
.completion(.finished),
|
||||
.completion(.finished),
|
||||
.completion(.failure(.oops)),
|
||||
.completion(.failure(.oops)),
|
||||
.value(6000),
|
||||
.value(7)])
|
||||
XCTAssertEqual(subscription3.history, [.cancelled])
|
||||
}
|
||||
|
||||
func testConcatenateTwoSequences() {
|
||||
let sequence1: Publishers.Sequence =
|
||||
sequence(first: 1, next: { $0 > 20 ? nil : $0 * 2 })
|
||||
.publisher
|
||||
|
||||
let sequence2: Publishers.Sequence = (33 ..< 40).publisher
|
||||
|
||||
let expected = [1, 2, 4, 8, 16, 32, 33, 34, 35, 36, 37, 38, 39]
|
||||
|
||||
var historyAppend = [Int]()
|
||||
var appendCompleted = false
|
||||
let append: Publishers.Concatenate = sequence1.append(sequence2)
|
||||
let cancellableAppend = append
|
||||
.sink(receiveCompletion: { _ in appendCompleted = true },
|
||||
receiveValue: { historyAppend.append($0) })
|
||||
XCTAssertEqual(historyAppend, expected)
|
||||
XCTAssertTrue(appendCompleted)
|
||||
cancellableAppend.cancel()
|
||||
|
||||
var historyPrepend = [Int]()
|
||||
var prependCompleted = false
|
||||
let prepend: Publishers.Concatenate = sequence2.prepend(sequence1)
|
||||
let cancellablePrepend = prepend
|
||||
.sink(receiveCompletion: { _ in prependCompleted = true },
|
||||
receiveValue: { historyPrepend.append($0) })
|
||||
XCTAssertEqual(historyPrepend, [1, 2, 4, 8, 16, 32, 33, 34, 35, 36, 37, 38, 39])
|
||||
XCTAssertTrue(prependCompleted)
|
||||
cancellablePrepend.cancel()
|
||||
}
|
||||
|
||||
func testPrefixFailureFailsDownstream() {
|
||||
let subscription1 = CustomSubscription()
|
||||
let subscription2 = CustomSubscription()
|
||||
let publisher1 = CustomPublisher(subscription: subscription1)
|
||||
let publisher2 = CustomPublisher(subscription: subscription2)
|
||||
|
||||
let append = publisher1.append(publisher2)
|
||||
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: { $0.request(.max(10)) },
|
||||
receiveValue: { .max($0) },
|
||||
receiveCompletion: { _ in }
|
||||
)
|
||||
|
||||
append.subscribe(tracking)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Concatenate")])
|
||||
XCTAssertEqual(subscription1.history, [.requested(.max(10))])
|
||||
XCTAssertEqual(subscription2.history, [])
|
||||
|
||||
publisher1.send(completion: .failure(.oops))
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Concatenate"),
|
||||
.completion(.failure(.oops))])
|
||||
XCTAssertEqual(subscription1.history, [.requested(.max(10))])
|
||||
XCTAssertEqual(subscription2.history, [])
|
||||
|
||||
XCTAssertEqual(publisher1.send(2), .max(2))
|
||||
publisher1.send(completion: .finished)
|
||||
XCTAssertEqual(publisher2.send(3), .max(3))
|
||||
publisher2.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Concatenate"),
|
||||
.completion(.failure(.oops)),
|
||||
.value(2),
|
||||
.value(3),
|
||||
.completion(.finished)])
|
||||
XCTAssertEqual(subscription1.history, [.requested(.max(10))])
|
||||
XCTAssertEqual(subscription2.history, [.requested(.max(11))])
|
||||
}
|
||||
|
||||
func testSubscribesToUpstreamThenSendsSubscriptionDownstream() {
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let append = publisher.append(1, 2, 3)
|
||||
let tracking = TrackingSubscriber()
|
||||
|
||||
var didSubscribe = false
|
||||
|
||||
publisher.didSubscribe = { _ in
|
||||
XCTAssertEqual(tracking.history, [.subscription("Concatenate")])
|
||||
didSubscribe = true
|
||||
}
|
||||
|
||||
XCTAssertEqual(tracking.history, [])
|
||||
|
||||
append.subscribe(tracking)
|
||||
|
||||
XCTAssertTrue(didSubscribe)
|
||||
XCTAssertEqual(tracking.history, [.subscription("Concatenate")])
|
||||
}
|
||||
|
||||
func testBackpressure() throws {
|
||||
let subscription1 = CustomSubscription()
|
||||
let subscription2 = CustomSubscription()
|
||||
let publisher1 = CustomPublisher(subscription: subscription1)
|
||||
let publisher2 = CustomPublisher(subscription: subscription2)
|
||||
|
||||
let append = publisher1.append(publisher2)
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: { downstreamSubscription = $0; $0.request(.max(10)) },
|
||||
receiveValue: { .max($0) },
|
||||
receiveCompletion: { _ in }
|
||||
)
|
||||
|
||||
publisher2.willSubscribe = { _ in
|
||||
downstreamSubscription?.request(.max(7))
|
||||
XCTAssertEqual(subscription1.history, [.requested(.max(10)),
|
||||
.requested(.none)])
|
||||
XCTAssertEqual(subscription2.history, [])
|
||||
}
|
||||
|
||||
append.subscribe(tracking)
|
||||
try XCTUnwrap(downstreamSubscription).request(.none)
|
||||
XCTAssertEqual(publisher1.send(3), .max(3))
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Concatenate"),
|
||||
.value(3)])
|
||||
XCTAssertEqual(subscription1.history, [.requested(.max(10)),
|
||||
.requested(.none)])
|
||||
XCTAssertEqual(subscription2.history, [])
|
||||
|
||||
publisher1.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(subscription1.history, [.requested(.max(10)),
|
||||
.requested(.none)])
|
||||
XCTAssertEqual(subscription2.history, [.requested(.max(19))])
|
||||
|
||||
try XCTUnwrap(downstreamSubscription).request(.unlimited)
|
||||
|
||||
XCTAssertEqual(subscription1.history, [.requested(.max(10)),
|
||||
.requested(.none)])
|
||||
XCTAssertEqual(subscription2.history, [.requested(.max(19)),
|
||||
.requested(.unlimited)])
|
||||
|
||||
publisher2.send(completion: .finished)
|
||||
|
||||
try XCTUnwrap(downstreamSubscription).request(.max(42))
|
||||
|
||||
XCTAssertEqual(subscription1.history, [.requested(.max(10)),
|
||||
.requested(.none)])
|
||||
XCTAssertEqual(subscription2.history, [.requested(.max(19)),
|
||||
.requested(.unlimited),
|
||||
.requested(.max(42))])
|
||||
}
|
||||
|
||||
func testCancelAlreadyCancelled() throws {
|
||||
let subscription1 = CustomSubscription()
|
||||
let subscription2 = CustomSubscription()
|
||||
let publisher1 = CustomPublisher(subscription: subscription1)
|
||||
let publisher2 = CustomPublisher(subscription: subscription2)
|
||||
|
||||
let append = publisher1.append(publisher2)
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: { downstreamSubscription = $0 },
|
||||
receiveValue: { .max($0) },
|
||||
receiveCompletion: { _ in }
|
||||
)
|
||||
append.subscribe(tracking)
|
||||
try XCTUnwrap(downstreamSubscription).cancel()
|
||||
try XCTUnwrap(downstreamSubscription).cancel()
|
||||
try XCTUnwrap(downstreamSubscription).request(.max(2)) // total demand is 2
|
||||
try XCTUnwrap(downstreamSubscription).cancel()
|
||||
try XCTUnwrap(downstreamSubscription).request(.max(3)) // total demand is 5
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Concatenate")])
|
||||
XCTAssertEqual(subscription1.history, [.cancelled])
|
||||
XCTAssertEqual(subscription2.history, [])
|
||||
|
||||
XCTAssertEqual(publisher1.send(0), .none) // total demand is 4
|
||||
publisher1.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Concatenate"),
|
||||
.value(0)])
|
||||
XCTAssertEqual(subscription1.history, [.cancelled])
|
||||
XCTAssertEqual(subscription2.history, [.requested(.max(4))])
|
||||
|
||||
XCTAssertEqual(publisher1.send(0), .none) // total demand is 3
|
||||
publisher1.send(completion: .failure(.oops))
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Concatenate"),
|
||||
.value(0),
|
||||
.value(0),
|
||||
.completion(.failure(.oops))])
|
||||
XCTAssertEqual(subscription1.history, [.cancelled])
|
||||
XCTAssertEqual(subscription2.history, [.requested(.max(4))])
|
||||
|
||||
try XCTUnwrap(downstreamSubscription).cancel()
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Concatenate"),
|
||||
.value(0),
|
||||
.value(0),
|
||||
.completion(.failure(.oops))])
|
||||
XCTAssertEqual(subscription1.history, [.cancelled])
|
||||
XCTAssertEqual(subscription2.history, [.requested(.max(4)),
|
||||
.cancelled])
|
||||
|
||||
let subscription3 = CustomSubscription()
|
||||
publisher2.send(subscription: subscription3)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Concatenate"),
|
||||
.value(0),
|
||||
.value(0),
|
||||
.completion(.failure(.oops))])
|
||||
XCTAssertEqual(subscription1.history, [.cancelled])
|
||||
XCTAssertEqual(subscription2.history, [.requested(.max(4)),
|
||||
.cancelled])
|
||||
XCTAssertEqual(subscription3.history, [.cancelled])
|
||||
}
|
||||
|
||||
func testRecursivelyReceiveValue() {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .none,
|
||||
createSut: { $0.append() })
|
||||
|
||||
var recursion = 10
|
||||
helper.tracking.onValue = {
|
||||
if recursion == 0 { return }
|
||||
recursion -= 1
|
||||
XCTAssertEqual(helper.publisher.send($0 + 1), .none)
|
||||
}
|
||||
|
||||
assertCrashes {
|
||||
XCTAssertEqual(helper.publisher.send(1), .none)
|
||||
}
|
||||
}
|
||||
|
||||
func testRecursivelyReceiveFailure() {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .none,
|
||||
createSut: { $0.append() })
|
||||
|
||||
var recursion = 10
|
||||
helper.tracking.onFailure = { _ in
|
||||
if recursion == 0 { return }
|
||||
recursion -= 1
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
}
|
||||
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Concatenate"),
|
||||
.completion(.failure(.oops)),
|
||||
.completion(.failure(.oops)),
|
||||
.completion(.failure(.oops)),
|
||||
.completion(.failure(.oops)),
|
||||
.completion(.failure(.oops)),
|
||||
.completion(.failure(.oops)),
|
||||
.completion(.failure(.oops)),
|
||||
.completion(.failure(.oops)),
|
||||
.completion(.failure(.oops)),
|
||||
.completion(.failure(.oops)),
|
||||
.completion(.failure(.oops))])
|
||||
XCTAssertEqual(helper.subscription.history, [])
|
||||
}
|
||||
|
||||
func testHelperMethods() {
|
||||
let publisher = CustomPublisher(subscription: nil)
|
||||
XCTAssertEqual(publisher.append(2, 3, 5, 7).suffix.sequence, [2, 3, 5, 7])
|
||||
XCTAssertEqual(publisher.append(CollectionOfOne(42)).suffix.sequence.first, 42)
|
||||
XCTAssertEqual(publisher.prepend(7, 5, 3, 2).prefix.sequence, [7, 5, 3, 2])
|
||||
XCTAssertEqual(publisher.prepend(CollectionOfOne(42)).prefix.sequence.first, 42)
|
||||
}
|
||||
|
||||
func testConcatenateReceiveValueBeforeSubscription() {
|
||||
testReceiveValueBeforeSubscription(value: 12,
|
||||
expected: .crash,
|
||||
{ $0.append(1, 2, 3) })
|
||||
|
||||
testReceiveValueBeforeSubscription(value: 12,
|
||||
expected: .crash,
|
||||
{ $0.prepend(1, 2, 3) })
|
||||
}
|
||||
|
||||
func testConcatenateReceiveCompletionBeforeSubscription() {
|
||||
testReceiveCompletionBeforeSubscription(
|
||||
inputType: Int.self,
|
||||
expected: .history([.subscription("Concatenate")]),
|
||||
{ $0.append(1, 2, 3) }
|
||||
)
|
||||
|
||||
testReceiveCompletionBeforeSubscription(
|
||||
inputType: Int.self,
|
||||
expected: .history([.subscription("Concatenate")]),
|
||||
{ $0.prepend(1, 2, 3) }
|
||||
)
|
||||
}
|
||||
|
||||
func testConcatenateRequestBeforeSubscription() {
|
||||
testRequestBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
{ $0.append(1, 2, 3) })
|
||||
}
|
||||
|
||||
func testConcatenateCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
{ $0.append(1, 2, 3) })
|
||||
}
|
||||
|
||||
func testConcatenateReceiveSubscriptionTwice() throws {
|
||||
try testReceiveSubscriptionTwice { $0.append(1, 2, 3) }
|
||||
}
|
||||
|
||||
func testConcatenateReflection() throws {
|
||||
try testReflection(parentInput: Float.self,
|
||||
parentFailure: TestingError.self,
|
||||
description: "Concatenate",
|
||||
customMirror: expectedChildren(
|
||||
("downstream", .contains("TrackingSubscriberBase")),
|
||||
("upstreamSubscription", .anything),
|
||||
("suffix", .contains("(sequence: [2.0, 3.0, 5.0, 7.0])")),
|
||||
("demand", "max(0)")
|
||||
),
|
||||
playgroundDescription: "Concatenate",
|
||||
{ $0.append(2, 3, 5, 7) })
|
||||
}
|
||||
|
||||
func testConcatenateLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
finishingIsPassedThrough: true,
|
||||
{ $0.append() })
|
||||
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
finishingIsPassedThrough: false,
|
||||
{ $0.prepend(1, 2, 3) })
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
import XCTest
|
||||
|
||||
//import Combine
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
|
||||
@@ -0,0 +1,464 @@
|
||||
//
|
||||
// DelayTests.swift
|
||||
// OpenCombineTests
|
||||
//
|
||||
// Created by Евгений Богомолов on 08/09/2019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class DelayTests: XCTestCase {
|
||||
|
||||
// Delay's Inner doesn't conform to CustomStringConvertible, so we can't compare
|
||||
// subscriptions using their descriptions
|
||||
private let delaySubscription: StringSubscription = {
|
||||
let tracking = TrackingSubscriber()
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
CustomPublisher(subscription: CustomSubscription())
|
||||
.delay(for: 0, scheduler: scheduler)
|
||||
.subscribe(tracking)
|
||||
scheduler.executeScheduledActions()
|
||||
return tracking.subscriptions.first.map(StringSubscription.subscription)
|
||||
?? "Delay"
|
||||
}()
|
||||
|
||||
private let delaySubscriptionImmediateScheduler: StringSubscription = {
|
||||
let tracking = TrackingSubscriber()
|
||||
CustomPublisher(subscription: CustomSubscription())
|
||||
.delay(for: 0, scheduler: ImmediateScheduler.shared)
|
||||
.subscribe(tracking)
|
||||
return tracking.subscriptions.first.map(StringSubscription.subscription)
|
||||
?? "Delay"
|
||||
}()
|
||||
|
||||
func testBasicBehavior() {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(100),
|
||||
receiveValueDemand: .max(12)) {
|
||||
$0.delay(for: .nanoseconds(200),
|
||||
tolerance: .nanoseconds(5),
|
||||
scheduler: scheduler,
|
||||
options: .nontrivialOptions)
|
||||
}
|
||||
XCTAssertNotNil(helper.publisher.subscriber)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(delaySubscription)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(100))])
|
||||
XCTAssertEqual(scheduler.history, [])
|
||||
|
||||
scheduler.executeScheduledActions()
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(delaySubscription)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(100))])
|
||||
XCTAssertEqual(scheduler.history, [])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(1), .none)
|
||||
XCTAssertEqual(helper.publisher.send(2), .none)
|
||||
XCTAssertEqual(helper.publisher.send(3), .none)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(delaySubscription)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(100))])
|
||||
XCTAssertEqual(scheduler.scheduledDates, [.nanoseconds(200),
|
||||
.nanoseconds(200),
|
||||
.nanoseconds(200)])
|
||||
|
||||
XCTAssertEqual(scheduler.history,
|
||||
[.now,
|
||||
.scheduleAfterDate(.nanoseconds(200),
|
||||
tolerance: .nanoseconds(5),
|
||||
options: .nontrivialOptions),
|
||||
.now,
|
||||
.scheduleAfterDate(.nanoseconds(200),
|
||||
tolerance: .nanoseconds(5),
|
||||
options: .nontrivialOptions),
|
||||
.now,
|
||||
.scheduleAfterDate(.nanoseconds(200),
|
||||
tolerance: .nanoseconds(5),
|
||||
options: .nontrivialOptions)])
|
||||
|
||||
scheduler.executeScheduledActions()
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(delaySubscription),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3)])
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(100)),
|
||||
.requested(.max(12)),
|
||||
.requested(.max(12)),
|
||||
.requested(.max(12))])
|
||||
|
||||
XCTAssertEqual(scheduler.history,
|
||||
[.now,
|
||||
.scheduleAfterDate(.nanoseconds(200),
|
||||
tolerance: .nanoseconds(5),
|
||||
options: .nontrivialOptions),
|
||||
.now,
|
||||
.scheduleAfterDate(.nanoseconds(200),
|
||||
tolerance: .nanoseconds(5),
|
||||
options: .nontrivialOptions),
|
||||
.now,
|
||||
.scheduleAfterDate(.nanoseconds(200),
|
||||
tolerance: .nanoseconds(5),
|
||||
options: .nontrivialOptions)])
|
||||
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
helper.publisher.send(completion: .finished)
|
||||
XCTAssertEqual(helper.publisher.send(4), .none)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(delaySubscription),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(100)),
|
||||
.requested(.max(12)),
|
||||
.requested(.max(12)),
|
||||
.requested(.max(12))])
|
||||
XCTAssertEqual(scheduler.scheduledDates, [.nanoseconds(400)])
|
||||
XCTAssertEqual(scheduler.history,
|
||||
[.now,
|
||||
.scheduleAfterDate(.nanoseconds(200),
|
||||
tolerance: .nanoseconds(5),
|
||||
options: .nontrivialOptions),
|
||||
.now,
|
||||
.scheduleAfterDate(.nanoseconds(200),
|
||||
tolerance: .nanoseconds(5),
|
||||
options: .nontrivialOptions),
|
||||
.now,
|
||||
.scheduleAfterDate(.nanoseconds(200),
|
||||
tolerance: .nanoseconds(5),
|
||||
options: .nontrivialOptions),
|
||||
.now,
|
||||
.scheduleAfterDate(.nanoseconds(400),
|
||||
tolerance: .nanoseconds(5),
|
||||
options: .nontrivialOptions)])
|
||||
|
||||
scheduler.executeScheduledActions()
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(delaySubscription),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3),
|
||||
.completion(.failure(.oops))])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(100)),
|
||||
.requested(.max(12)),
|
||||
.requested(.max(12)),
|
||||
.requested(.max(12))])
|
||||
XCTAssertEqual(scheduler.history,
|
||||
[.now,
|
||||
.scheduleAfterDate(.nanoseconds(200),
|
||||
tolerance: .nanoseconds(5),
|
||||
options: .nontrivialOptions),
|
||||
.now,
|
||||
.scheduleAfterDate(.nanoseconds(200),
|
||||
tolerance: .nanoseconds(5),
|
||||
options: .nontrivialOptions),
|
||||
.now,
|
||||
.scheduleAfterDate(.nanoseconds(200),
|
||||
tolerance: .nanoseconds(5),
|
||||
options: .nontrivialOptions),
|
||||
.now,
|
||||
.scheduleAfterDate(.nanoseconds(400),
|
||||
tolerance: .nanoseconds(5),
|
||||
options: .nontrivialOptions)])
|
||||
XCTAssertEqual(scheduler.now, .nanoseconds(400))
|
||||
}
|
||||
|
||||
func testRequest() throws {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .none) {
|
||||
$0.delay(for: .nanoseconds(200),
|
||||
tolerance: .nanoseconds(5),
|
||||
scheduler: scheduler,
|
||||
options: .nontrivialOptions)
|
||||
}
|
||||
scheduler.executeScheduledActions()
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(delaySubscription)])
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(10))
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(4))
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(5))
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.none)
|
||||
XCTAssertEqual(helper.publisher.send(2000), .none)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(delaySubscription)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(10)),
|
||||
.requested(.max(4)),
|
||||
.requested(.max(5)),
|
||||
.requested(.none)])
|
||||
|
||||
scheduler.executeScheduledActions()
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(delaySubscription),
|
||||
.value(2000)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(10)),
|
||||
.requested(.max(4)),
|
||||
.requested(.max(5)),
|
||||
.requested(.none)])
|
||||
}
|
||||
|
||||
func testCancelAlreadyCancelled() throws {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .none) {
|
||||
$0.delay(for: .nanoseconds(200),
|
||||
tolerance: .nanoseconds(5),
|
||||
scheduler: scheduler,
|
||||
options: .nontrivialOptions)
|
||||
}
|
||||
|
||||
scheduler.executeScheduledActions()
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(delaySubscription)])
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(42))
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited), .cancelled])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(delaySubscription)])
|
||||
XCTAssertEqual(scheduler.history, [])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(0), .none)
|
||||
helper.publisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited), .cancelled])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(delaySubscription)])
|
||||
XCTAssertEqual(scheduler.history, [])
|
||||
}
|
||||
|
||||
func testReceiveCompletionImmediatelyAfterSubscription() {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .none) {
|
||||
$0.delay(for: .nanoseconds(123),
|
||||
tolerance: .nanoseconds(5),
|
||||
scheduler: scheduler,
|
||||
options: .nontrivialOptions)
|
||||
}
|
||||
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(delaySubscription)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(scheduler.history,
|
||||
[.now,
|
||||
.scheduleAfterDate(.nanoseconds(123),
|
||||
tolerance: .nanoseconds(5),
|
||||
options: .nontrivialOptions)])
|
||||
|
||||
scheduler.executeScheduledActions()
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(delaySubscription),
|
||||
.completion(.failure(.oops))])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
}
|
||||
|
||||
func testReceiveCompletionImmediatelyAfterValue() {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .max(418)) {
|
||||
$0.delay(for: .nanoseconds(123),
|
||||
tolerance: .nanoseconds(5),
|
||||
scheduler: scheduler,
|
||||
options: .nontrivialOptions)
|
||||
}
|
||||
XCTAssertEqual(helper.publisher.send(-1), .none)
|
||||
scheduler.executeScheduledActions()
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(1000), .none)
|
||||
helper.publisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(delaySubscription),
|
||||
.value(-1)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
|
||||
.requested(.max(418))])
|
||||
XCTAssertEqual(scheduler.history,
|
||||
[.now,
|
||||
.scheduleAfterDate(.nanoseconds(123),
|
||||
tolerance: .nanoseconds(5),
|
||||
options: .nontrivialOptions),
|
||||
.now,
|
||||
.scheduleAfterDate(.nanoseconds(246),
|
||||
tolerance: .nanoseconds(5),
|
||||
options: .nontrivialOptions),
|
||||
.now,
|
||||
.scheduleAfterDate(.nanoseconds(246),
|
||||
tolerance: .nanoseconds(5),
|
||||
options: .nontrivialOptions)])
|
||||
|
||||
scheduler.executeScheduledActions()
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(delaySubscription),
|
||||
.value(-1),
|
||||
.value(1000),
|
||||
.completion(.finished)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
|
||||
.requested(.max(418))])
|
||||
}
|
||||
|
||||
func testCrashesWhenReceivingInputRecursively() {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .max(418)) {
|
||||
$0.delay(for: .nanoseconds(123), scheduler: ImmediateScheduler.shared)
|
||||
}
|
||||
|
||||
var recursionCounter = 5
|
||||
helper.tracking.onValue = { _ in
|
||||
if recursionCounter == 0 { return }
|
||||
recursionCounter -= 1
|
||||
_ = helper.publisher.send(-1)
|
||||
}
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(0), .none)
|
||||
XCTAssertEqual(helper.tracking.history,
|
||||
[.subscription(delaySubscriptionImmediateScheduler),
|
||||
.value(0),
|
||||
.value(-1),
|
||||
.value(-1),
|
||||
.value(-1),
|
||||
.value(-1),
|
||||
.value(-1)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
|
||||
.requested(.max(418)),
|
||||
.requested(.max(418)),
|
||||
.requested(.max(418)),
|
||||
.requested(.max(418)),
|
||||
.requested(.max(418)),
|
||||
.requested(.max(418))])
|
||||
}
|
||||
|
||||
func testReceiveCompletionRecursively() {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .max(418)) {
|
||||
$0.delay(for: .nanoseconds(123), scheduler: ImmediateScheduler.shared)
|
||||
}
|
||||
helper.tracking.onFinish = {
|
||||
helper.publisher.send(completion: .finished)
|
||||
}
|
||||
helper.publisher.send(completion: .finished)
|
||||
}
|
||||
|
||||
func testStrongCaptureWhenSchedulingValue() {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
var value: Int?
|
||||
var subscriberReleased = false
|
||||
do {
|
||||
let publisher = CustomPublisher(subscription: CustomSubscription())
|
||||
let delay = publisher.delay(for: 0.35, scheduler: scheduler)
|
||||
let tracking = TrackingSubscriber(receiveValue: { value = $0; return .none },
|
||||
onDeinit: { subscriberReleased = true })
|
||||
delay.subscribe(tracking)
|
||||
scheduler.executeScheduledActions()
|
||||
XCTAssertEqual(tracking.history, [.subscription(delaySubscription)])
|
||||
XCTAssertEqual(publisher.send(42), .none)
|
||||
XCTAssertEqual(tracking.history, [.subscription(delaySubscription)])
|
||||
XCTAssertEqual(scheduler.history,
|
||||
[.minimumTolerance,
|
||||
.now,
|
||||
.scheduleAfterDate(.seconds(0.35),
|
||||
tolerance: 0,
|
||||
options: nil)])
|
||||
tracking.cancel()
|
||||
publisher.cancel()
|
||||
}
|
||||
XCTAssertFalse(subscriberReleased)
|
||||
scheduler.executeScheduledActions()
|
||||
XCTAssertEqual(value, 42)
|
||||
XCTAssertTrue(subscriberReleased)
|
||||
}
|
||||
|
||||
func testStrongCaptureWhenSchedulingCompletion() {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
var completion: Subscribers.Completion<TestingError>?
|
||||
var subscriberReleased = false
|
||||
do {
|
||||
let publisher = CustomPublisher(subscription: CustomSubscription())
|
||||
let delay = publisher.delay(for: 0.35, scheduler: scheduler)
|
||||
let tracking = TrackingSubscriber(receiveCompletion: { completion = $0 },
|
||||
onDeinit: { subscriberReleased = true })
|
||||
delay.subscribe(tracking)
|
||||
scheduler.executeScheduledActions()
|
||||
XCTAssertEqual(tracking.history, [.subscription(delaySubscription)])
|
||||
publisher.send(completion: .finished)
|
||||
XCTAssertEqual(tracking.history, [.subscription(delaySubscription)])
|
||||
XCTAssertEqual(scheduler.history,
|
||||
[.minimumTolerance,
|
||||
.now,
|
||||
.scheduleAfterDate(.seconds(0.35),
|
||||
tolerance: 0,
|
||||
options: nil)])
|
||||
tracking.cancel()
|
||||
publisher.cancel()
|
||||
}
|
||||
XCTAssertFalse(subscriberReleased)
|
||||
scheduler.executeScheduledActions()
|
||||
XCTAssertEqual(completion, .finished)
|
||||
XCTAssertTrue(subscriberReleased)
|
||||
}
|
||||
|
||||
func testDelayReceiveSubscriptionTwice() throws {
|
||||
try testReceiveSubscriptionTwice {
|
||||
$0.delay(for: 0.35, scheduler: ImmediateScheduler.shared)
|
||||
}
|
||||
}
|
||||
|
||||
func testDelayReceiveValueBeforeSubscription() {
|
||||
testReceiveValueBeforeSubscription(value: 213,
|
||||
expected: .history([], demand: .none)) {
|
||||
$0.delay(for: 0.35, scheduler: ImmediateScheduler.shared)
|
||||
}
|
||||
}
|
||||
|
||||
func testDelayReceiveCompletionBeforeSubscription() {
|
||||
testReceiveCompletionBeforeSubscription(inputType: Int.self,
|
||||
expected: .history([])) {
|
||||
$0.delay(for: 0.35, scheduler: ImmediateScheduler.shared)
|
||||
}
|
||||
}
|
||||
|
||||
func testDelayRequestBeforeSubscription() {
|
||||
testRequestBeforeSubscription(inputType: Int.self, shouldCrash: false) {
|
||||
$0.delay(for: 0.35, scheduler: ImmediateScheduler.shared)
|
||||
}
|
||||
}
|
||||
|
||||
func testDelayCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self, shouldCrash: false) {
|
||||
$0.delay(for: 0.35, scheduler: ImmediateScheduler.shared)
|
||||
}
|
||||
}
|
||||
|
||||
func testDelayReflection() throws {
|
||||
// Delay's Inner doesn't customize its reflection
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: Error.self,
|
||||
description: nil,
|
||||
customMirror: nil,
|
||||
playgroundDescription: nil) {
|
||||
$0.delay(for: 42, scheduler: ImmediateScheduler.shared)
|
||||
}
|
||||
}
|
||||
|
||||
func testDelayLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: true) {
|
||||
$0.delay(for: 42, scheduler: ImmediateScheduler.shared)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,359 @@
|
||||
//
|
||||
// DropUntilOutputTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 24.12.2019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class DropUntilOutputTests: XCTestCase {
|
||||
|
||||
func testOtherCompletesBeforeTriggering() {
|
||||
let otherSubscription = CustomSubscription()
|
||||
let otherPublisher = CustomPublisher(subscription: otherSubscription)
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(100),
|
||||
receiveValueDemand: .max(5),
|
||||
createSut: { $0.drop(untilOutputFrom: otherPublisher) }
|
||||
)
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(1), .none)
|
||||
XCTAssertEqual(helper.publisher.send(2), .none)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("DropUntilOutput")])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(100))])
|
||||
XCTAssertEqual(otherSubscription.history, [.requested(.max(1))])
|
||||
|
||||
otherPublisher.send(completion: .finished)
|
||||
otherPublisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("DropUntilOutput"),
|
||||
.completion(.finished),
|
||||
.completion(.finished)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(100)), .cancelled])
|
||||
XCTAssertEqual(otherSubscription.history, [.requested(.max(1))])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(3), .none)
|
||||
XCTAssertEqual(otherPublisher.send(1000), .none)
|
||||
XCTAssertEqual(helper.publisher.send(4), .max(5))
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("DropUntilOutput"),
|
||||
.completion(.finished),
|
||||
.completion(.finished),
|
||||
.value(4)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(100)), .cancelled])
|
||||
XCTAssertEqual(otherSubscription.history, [.requested(.max(1))])
|
||||
}
|
||||
|
||||
func testOtherFailsAfterTriggering() {
|
||||
let otherSubscription = CustomSubscription()
|
||||
let otherPublisher = CustomPublisher(subscription: otherSubscription)
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(2),
|
||||
receiveValueDemand: .max(5),
|
||||
createSut: { $0.drop(untilOutputFrom: otherPublisher) }
|
||||
)
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(1), .none)
|
||||
XCTAssertEqual(otherPublisher.send(1000), .none)
|
||||
XCTAssertEqual(helper.publisher.send(2), .max(5))
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("DropUntilOutput"),
|
||||
.value(2)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(2))])
|
||||
XCTAssertEqual(otherSubscription.history, [.requested(.max(1))])
|
||||
|
||||
otherPublisher.send(completion: .failure(.oops))
|
||||
otherPublisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("DropUntilOutput"),
|
||||
.value(2)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(2))])
|
||||
XCTAssertEqual(otherSubscription.history, [.requested(.max(1))])
|
||||
}
|
||||
|
||||
func testDemand() throws {
|
||||
let subscription = CustomSubscription()
|
||||
let otherSubscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let otherPublisher = CustomPublisher(subscription: otherSubscription)
|
||||
let dropUntilOutput = publisher.drop(untilOutputFrom: otherPublisher)
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: { subscription in
|
||||
downstreamSubscription = subscription
|
||||
},
|
||||
receiveValue: { .max($0) }
|
||||
)
|
||||
dropUntilOutput.subscribe(tracking)
|
||||
|
||||
XCTAssertEqual(subscription.history, [])
|
||||
XCTAssertEqual(otherSubscription.history, [.requested(.max(1))])
|
||||
XCTAssertEqual(tracking.history, [.subscription("DropUntilOutput")])
|
||||
|
||||
try XCTUnwrap(downstreamSubscription).request(.max(4))
|
||||
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(4))])
|
||||
XCTAssertEqual(otherSubscription.history, [.requested(.max(1))])
|
||||
XCTAssertEqual(tracking.history, [.subscription("DropUntilOutput")])
|
||||
|
||||
XCTAssertEqual(publisher.send(1), .none)
|
||||
XCTAssertEqual(publisher.send(2), .none)
|
||||
XCTAssertEqual(otherPublisher.send(1000), .none)
|
||||
XCTAssertEqual(publisher.send(3), .max(3))
|
||||
XCTAssertEqual(publisher.send(4), .max(4))
|
||||
XCTAssertEqual(publisher.send(5), .max(5))
|
||||
XCTAssertEqual(publisher.send(6), .max(6))
|
||||
XCTAssertEqual(publisher.send(7), .max(7))
|
||||
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(4))])
|
||||
XCTAssertEqual(otherSubscription.history, [.requested(.max(1))])
|
||||
XCTAssertEqual(tracking.history, [.subscription("DropUntilOutput"),
|
||||
.value(3),
|
||||
.value(4),
|
||||
.value(5),
|
||||
.value(6),
|
||||
.value(7)])
|
||||
}
|
||||
|
||||
func testCancelAlreadyCancelled() throws {
|
||||
let otherSubscription = CustomSubscription()
|
||||
let otherPublisher = CustomPublisher(subscription: otherSubscription)
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(2),
|
||||
receiveValueDemand: .max(5),
|
||||
createSut: { $0.drop(untilOutputFrom: otherPublisher) }
|
||||
)
|
||||
|
||||
helper.subscription.onCancel = {
|
||||
XCTAssertEqual(otherSubscription.history, [.requested(.max(1))])
|
||||
}
|
||||
|
||||
otherSubscription.onCancel = {
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(2)), .cancelled])
|
||||
}
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(10))
|
||||
XCTAssertEqual(helper.publisher.send(1000), .none)
|
||||
helper.publisher.send(completion: .finished)
|
||||
|
||||
let subscription2 = CustomSubscription()
|
||||
helper.publisher.send(subscription: subscription2)
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(2)), .cancelled])
|
||||
XCTAssertEqual(otherSubscription.history, [.requested(.max(1)), .cancelled])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("DropUntilOutput")])
|
||||
XCTAssertEqual(subscription2.history, [.cancelled])
|
||||
}
|
||||
|
||||
func testSubscribesToOtherFirst() {
|
||||
let subscription = CustomSubscription()
|
||||
let otherSubscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let otherPublisher = CustomPublisher(subscription: otherSubscription)
|
||||
let dropUntilOutput = publisher.drop(untilOutputFrom: otherPublisher)
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: { _ in
|
||||
XCTAssertNil(publisher.subscriber)
|
||||
XCTAssertNil(otherPublisher.subscriber)
|
||||
XCTAssertEqual(subscription.history, [])
|
||||
XCTAssertEqual(otherSubscription.history, [])
|
||||
}
|
||||
)
|
||||
|
||||
otherPublisher.willSubscribe = { _ in
|
||||
XCTAssertNil(publisher.subscriber)
|
||||
}
|
||||
|
||||
publisher.willSubscribe = { _ in
|
||||
XCTAssertNotNil(otherPublisher.subscriber)
|
||||
}
|
||||
|
||||
dropUntilOutput.subscribe(tracking)
|
||||
tracking.cancel()
|
||||
}
|
||||
|
||||
func testSubscribersHaveTheSameCombineIdentifier() {
|
||||
let subscription = CustomSubscription()
|
||||
let otherSubscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let otherPublisher = CustomPublisher(subscription: otherSubscription)
|
||||
let dropUntilOutput = publisher.drop(untilOutputFrom: otherPublisher)
|
||||
let tracking = TrackingSubscriber()
|
||||
dropUntilOutput.subscribe(tracking)
|
||||
|
||||
XCTAssert(publisher.erasedSubscriber is CustomCombineIdentifierConvertible)
|
||||
XCTAssert(otherPublisher.erasedSubscriber is CustomCombineIdentifierConvertible)
|
||||
XCTAssertEqual(
|
||||
(publisher.erasedSubscriber as? CustomCombineIdentifierConvertible)?
|
||||
.combineIdentifier,
|
||||
(otherPublisher.erasedSubscriber as? CustomCombineIdentifierConvertible)?
|
||||
.combineIdentifier
|
||||
)
|
||||
}
|
||||
|
||||
func testLateSubscription() throws {
|
||||
|
||||
// This publisher doesn't send a subscription when it receives a subscriber
|
||||
let publisher = CustomPublisher(subscription: nil)
|
||||
let dropUntilOutput = publisher.drop(untilOutputFrom: Empty<Void, TestingError>())
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: {
|
||||
$0.request(.max(10))
|
||||
$0.request(.max(4))
|
||||
$0.request(.none)
|
||||
}
|
||||
)
|
||||
|
||||
dropUntilOutput.subscribe(tracking)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("DropUntilOutput"),
|
||||
.completion(.finished)])
|
||||
|
||||
let subscription = CustomSubscription()
|
||||
try XCTUnwrap(publisher.subscriber).receive(subscription: subscription)
|
||||
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(14))])
|
||||
XCTAssertEqual(tracking.history, [.subscription("DropUntilOutput"),
|
||||
.completion(.finished)])
|
||||
}
|
||||
|
||||
func testReusableOtherSubscriber() throws {
|
||||
let otherSubscription = CustomSubscription()
|
||||
let otherPublisher = CustomPublisher(subscription: otherSubscription)
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(2),
|
||||
receiveValueDemand: .max(5),
|
||||
createSut: { $0.drop(untilOutputFrom: otherPublisher) }
|
||||
)
|
||||
|
||||
let subscription2 = CustomSubscription()
|
||||
try XCTUnwrap(otherPublisher.subscriber).receive(subscription: subscription2)
|
||||
|
||||
XCTAssertEqual(subscription2.history, [.cancelled])
|
||||
XCTAssertEqual(otherPublisher.send(1000), .none)
|
||||
|
||||
let subscription3 = CustomSubscription()
|
||||
try XCTUnwrap(otherPublisher.subscriber).receive(subscription: subscription3)
|
||||
|
||||
XCTAssertEqual(subscription3.history, [.requested(.max(1))])
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
|
||||
XCTAssertEqual(subscription3.history, [.requested(.max(1)),
|
||||
.cancelled])
|
||||
XCTAssertEqual(otherSubscription.history, [.requested(.max(1))])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(2)), .cancelled])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("DropUntilOutput")])
|
||||
}
|
||||
|
||||
func testCrashesWhenReceivesInputAfterCancel() {
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .none,
|
||||
createSut: { $0.drop(untilOutputFrom: Empty<Void, TestingError>()) }
|
||||
)
|
||||
|
||||
assertCrashes {
|
||||
_ = helper.publisher.send(0)
|
||||
}
|
||||
}
|
||||
|
||||
func testDropUntilOutputReceiveValueBeforeSubscription() {
|
||||
testReceiveValueBeforeSubscription(
|
||||
value: 42,
|
||||
expected: .crash,
|
||||
{ $0.drop(untilOutputFrom: Empty<Int, Never>()) }
|
||||
)
|
||||
}
|
||||
|
||||
func testDropUntilOutputOtherReceiveValueBeforeSubscription() {
|
||||
testReceiveValueBeforeSubscription(
|
||||
value: 42,
|
||||
expected: .history([.subscription("DropUntilOutput"), .completion(.finished)],
|
||||
demand: .none),
|
||||
{ Empty<Int, Never>().drop(untilOutputFrom: $0) }
|
||||
)
|
||||
}
|
||||
|
||||
func testDropUntilOutputReceiveCompletionBeforeSubscription() {
|
||||
testReceiveCompletionBeforeSubscription(
|
||||
inputType: Int.self,
|
||||
expected: .history([.subscription("DropUntilOutput"),
|
||||
.completion(.finished),
|
||||
.completion(.finished)]),
|
||||
{ $0.drop(untilOutputFrom: Empty<Int, Never>()) }
|
||||
)
|
||||
}
|
||||
|
||||
func testDropUntilOutputOtherReceiveCompletionBeforeSubscription() {
|
||||
testReceiveCompletionBeforeSubscription(
|
||||
inputType: Int.self,
|
||||
expected: .history([.subscription("DropUntilOutput"),
|
||||
.completion(.finished),
|
||||
.completion(.finished)]),
|
||||
{ Empty<Int, Never>().drop(untilOutputFrom: $0) }
|
||||
)
|
||||
}
|
||||
|
||||
func testDropUntilOutputRequestBeforeSubscription() {
|
||||
testRequestBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
{ $0.drop(untilOutputFrom: Empty<Int, Never>()) })
|
||||
}
|
||||
|
||||
func testDropUntilOutputCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
{ $0.drop(untilOutputFrom: Empty<Int, Never>()) })
|
||||
}
|
||||
|
||||
func testDropUntilOutputReceiveSubscriptionTwice() throws {
|
||||
try testReceiveSubscriptionTwice {
|
||||
$0.drop(untilOutputFrom: Empty<Int, TestingError>())
|
||||
}
|
||||
}
|
||||
|
||||
func testDropUntilOutputLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
{ $0.drop(untilOutputFrom: Empty<Int, TestingError>()) })
|
||||
}
|
||||
|
||||
func testDropUntilOutputOtherLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
{ Empty<Int, TestingError>().drop(untilOutputFrom: $0) })
|
||||
}
|
||||
|
||||
func testDropUntilOutputReflection() throws {
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: TestingError.self,
|
||||
description: "DropUntilOutput",
|
||||
customMirror: childrenIsEmpty,
|
||||
playgroundDescription: "DropUntilOutput",
|
||||
{ $0.drop(untilOutputFrom: Empty<Int, TestingError>()) })
|
||||
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: TestingError.self,
|
||||
description: "DropUntilOutput",
|
||||
customMirror: childrenIsEmpty,
|
||||
playgroundDescription: "DropUntilOutput",
|
||||
{ Empty<Int, TestingError>().drop(untilOutputFrom: $0) })
|
||||
}
|
||||
}
|
||||
@@ -155,7 +155,7 @@ final class EncodeTests: XCTestCase {
|
||||
customMirror: expectedChildren(
|
||||
("downstream", .contains("TrackingSubscriberBase")),
|
||||
("finished", "false"),
|
||||
("upstreamSubscription", "nil")
|
||||
("upstreamSubscription", .anything)
|
||||
),
|
||||
playgroundDescription: "Encode",
|
||||
{ $0.encode(encoder: encoder) })
|
||||
@@ -275,7 +275,7 @@ final class EncodeTests: XCTestCase {
|
||||
customMirror: expectedChildren(
|
||||
("downstream", .contains("TrackingSubscriberBase")),
|
||||
("finished", "false"),
|
||||
("upstreamSubscription", "nil")
|
||||
("upstreamSubscription", .anything)
|
||||
),
|
||||
playgroundDescription: "Decode",
|
||||
{ $0.decode(type: Int.self, decoder: decoder) })
|
||||
|
||||
@@ -75,7 +75,7 @@ final class FlatMapTests: XCTestCase {
|
||||
XCTAssertNil(upstream.erasedSubscriber)
|
||||
}
|
||||
)
|
||||
upstream.onSubscribe = { _ in
|
||||
upstream.willSubscribe = { _ in
|
||||
upstreamReceivedSubscriber = true
|
||||
XCTAssertEqual(tracking.history, [.subscription("FlatMap")])
|
||||
}
|
||||
@@ -142,32 +142,43 @@ final class FlatMapTests: XCTestCase {
|
||||
// Simply making it here shows that there's no dealock
|
||||
}
|
||||
|
||||
func testCancelCancels() {
|
||||
func testCancelCancels() throws {
|
||||
let upstreamSubscription = CustomSubscription()
|
||||
let upstreamPublisher = CustomPublisherBase<Int, Never>(
|
||||
subscription: upstreamSubscription)
|
||||
subscription: upstreamSubscription
|
||||
)
|
||||
|
||||
let childSubscription = CustomSubscription()
|
||||
let childPublisher = CustomPublisherBase<Int, Never>(
|
||||
subscription: childSubscription)
|
||||
subscription: childSubscription
|
||||
)
|
||||
|
||||
let flatMap = upstreamPublisher.flatMap { _ in childPublisher }
|
||||
|
||||
var downstreamSubscription: Subscription?
|
||||
let downstreamSubscriber = TrackingSubscriberBase<Int, Never>(receiveSubscription:
|
||||
{
|
||||
downstreamSubscription = $0
|
||||
$0.request(.unlimited)
|
||||
})
|
||||
let downstreamSubscriber = TrackingSubscriberBase<Int, Never>(
|
||||
receiveSubscription: {
|
||||
downstreamSubscription = $0
|
||||
$0.request(.max(42))
|
||||
}
|
||||
)
|
||||
|
||||
upstreamSubscription.onCancel = {
|
||||
XCTAssertEqual(childSubscription.history, [.requested(.max(1)), .cancelled])
|
||||
}
|
||||
|
||||
childSubscription.onCancel = {
|
||||
XCTAssertEqual(upstreamSubscription.history, [.requested(.unlimited)])
|
||||
}
|
||||
|
||||
flatMap.subscribe(downstreamSubscriber)
|
||||
|
||||
XCTAssertEqual(upstreamPublisher.send(1), .none)
|
||||
|
||||
downstreamSubscription?.cancel()
|
||||
try XCTUnwrap(downstreamSubscription).cancel()
|
||||
|
||||
XCTAssertEqual(upstreamSubscription.history.last, .cancelled)
|
||||
XCTAssertEqual(childSubscription.history.last, .cancelled)
|
||||
XCTAssertEqual(upstreamSubscription.history, [.requested(.unlimited), .cancelled])
|
||||
XCTAssertEqual(childSubscription.history, [.requested(.max(1)), .cancelled])
|
||||
}
|
||||
|
||||
func testCancelTwice() throws {
|
||||
@@ -390,11 +401,15 @@ final class FlatMapTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testChildValueReceivedWhileSendingValue() throws {
|
||||
let upstreamPublisher = PassthroughSubject<AnyPublisher<Int, TestingError>,
|
||||
TestingError>()
|
||||
let upstreamSubscription = CustomSubscription()
|
||||
let upstreamPublisher = CustomPublisherBase<CustomPublisher, TestingError>(
|
||||
subscription: upstreamSubscription
|
||||
)
|
||||
|
||||
let child1Publisher = CustomPublisher(subscription: CustomSubscription())
|
||||
let child2Publisher = CustomPublisher(subscription: CustomSubscription())
|
||||
let childSubscription1 = CustomSubscription()
|
||||
let childSubscription2 = CustomSubscription()
|
||||
let child1Publisher = CustomPublisher(subscription: childSubscription1)
|
||||
let child2Publisher = CustomPublisher(subscription: childSubscription2)
|
||||
|
||||
let flatMap = upstreamPublisher.flatMap { $0 }
|
||||
|
||||
@@ -408,12 +423,17 @@ final class FlatMapTests: XCTestCase {
|
||||
|
||||
flatMap.subscribe(downstreamSubscriber)
|
||||
|
||||
upstreamPublisher.send(AnyPublisher(child1Publisher))
|
||||
upstreamPublisher.send(AnyPublisher(child2Publisher))
|
||||
XCTAssertEqual(upstreamPublisher.send(child1Publisher), .none)
|
||||
XCTAssertEqual(upstreamPublisher.send(child2Publisher), .none)
|
||||
|
||||
assertCrashes {
|
||||
XCTAssertEqual(child1Publisher.send(666), .max(1))
|
||||
}
|
||||
XCTAssertEqual(child1Publisher.send(666), .max(1))
|
||||
|
||||
XCTAssertEqual(upstreamSubscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(downstreamSubscriber.history, [.subscription("FlatMap"),
|
||||
.value(666),
|
||||
.value(777)])
|
||||
XCTAssertEqual(childSubscription1.history, [.requested(.max(1))])
|
||||
XCTAssertEqual(childSubscription2.history, [.requested(.max(1))])
|
||||
}
|
||||
|
||||
func testOuterLockReentrance() {
|
||||
@@ -427,10 +447,7 @@ final class FlatMapTests: XCTestCase {
|
||||
let childSubscription = CustomSubscription()
|
||||
let child = CustomPublisher(subscription: childSubscription)
|
||||
|
||||
// If Apple changes the implementation to use recursive lock,
|
||||
// we must make sure no stack overflow occurs here,
|
||||
// which will also be detected as a crash, which is not what we want.
|
||||
var recursionDepth = 10
|
||||
var recursionDepth = 5
|
||||
helper.subscription.onRequest = { _ in
|
||||
if recursionDepth <= 0 {
|
||||
return
|
||||
@@ -441,9 +458,17 @@ final class FlatMapTests: XCTestCase {
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(child), .none)
|
||||
|
||||
assertCrashes {
|
||||
child.send(completion: .finished)
|
||||
}
|
||||
child.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("FlatMap")])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(1)),
|
||||
.requested(.max(1)),
|
||||
.requested(.max(1)),
|
||||
.requested(.max(1)),
|
||||
.requested(.max(1)),
|
||||
.requested(.max(1)),
|
||||
.requested(.max(1))])
|
||||
XCTAssertEqual(childSubscription.history, [.requested(.max(1))])
|
||||
}
|
||||
|
||||
func testDownstreamLockReentrance() throws {
|
||||
@@ -462,9 +487,6 @@ final class FlatMapTests: XCTestCase {
|
||||
// Create some downstream demand
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(5))
|
||||
|
||||
// If Apple changes the implementation to use recursive lock,
|
||||
// we must make sure no stack overflow occurs here,
|
||||
// which will also be detected as a crash, which is not what we want.
|
||||
var recursionDepth = 10
|
||||
helper.tracking.onFailure = { _ in
|
||||
if recursionDepth <= 0 {
|
||||
@@ -474,10 +496,13 @@ final class FlatMapTests: XCTestCase {
|
||||
_ = child.send(1)
|
||||
}
|
||||
|
||||
// Expected deadlock
|
||||
assertCrashes {
|
||||
child.send(completion: .failure(.oops))
|
||||
}
|
||||
child.send(completion: .failure(.oops))
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("FlatMap"),
|
||||
.completion(.failure(.oops)),
|
||||
.value(1)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(1))])
|
||||
XCTAssertEqual(childSubscription.history, [.requested(.max(1))])
|
||||
}
|
||||
|
||||
func testCompletesProperlyWhenUpstreamOutlivesChildren() {
|
||||
@@ -564,7 +589,7 @@ final class FlatMapTests: XCTestCase {
|
||||
createSut: { $0.flatMap(maxPublishers: .max(1)) { $0 } }
|
||||
)
|
||||
|
||||
child.onSubscribe = { subscriber in
|
||||
child.willSubscribe = { subscriber in
|
||||
helper.publisher.send(completion: .finished)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
//
|
||||
// HandleEventsTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 03.12.2019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class HandleEventsTests: XCTestCase {
|
||||
|
||||
func testBasicBehavior() throws {
|
||||
var history = [Event<TestingError>]()
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let handleEvents = publisher.handleAllEvents { history.append($0) }
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: {
|
||||
XCTAssertNil(publisher.subscriber)
|
||||
downstreamSubscription = $0
|
||||
},
|
||||
receiveValue: { .max($0) }
|
||||
)
|
||||
handleEvents.subscribe(tracking)
|
||||
|
||||
XCTAssertNotNil(publisher.subscriber)
|
||||
XCTAssertEqual(tracking.history, [.subscription("HandleEvents")])
|
||||
XCTAssertEqual(subscription.history, [])
|
||||
XCTAssertEqual(history, [.receiveSubscription("CustomSubscription")])
|
||||
|
||||
XCTAssertEqual(publisher.send(0), .none)
|
||||
XCTAssertEqual(publisher.send(1), .max(1))
|
||||
XCTAssertEqual(publisher.send(2), .max(2))
|
||||
XCTAssertEqual(publisher.send(3), .max(3))
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("HandleEvents"),
|
||||
.value(0),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3)])
|
||||
XCTAssertEqual(subscription.history, [])
|
||||
XCTAssertEqual(history, [.receiveSubscription("CustomSubscription"),
|
||||
.receiveOutput(0),
|
||||
.receiveOutput(1),
|
||||
.receiveRequest(.max(1)),
|
||||
.receiveOutput(2),
|
||||
.receiveRequest(.max(2)),
|
||||
.receiveOutput(3),
|
||||
.receiveRequest(.max(3))])
|
||||
|
||||
try XCTUnwrap(downstreamSubscription).request(.max(14))
|
||||
try XCTUnwrap(downstreamSubscription).request(.max(10))
|
||||
try XCTUnwrap(downstreamSubscription).request(.none)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("HandleEvents"),
|
||||
.value(0),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3)])
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(14)),
|
||||
.requested(.max(10)),
|
||||
.requested(.none)])
|
||||
XCTAssertEqual(history, [.receiveSubscription("CustomSubscription"),
|
||||
.receiveOutput(0),
|
||||
.receiveOutput(1),
|
||||
.receiveRequest(.max(1)),
|
||||
.receiveOutput(2),
|
||||
.receiveRequest(.max(2)),
|
||||
.receiveOutput(3),
|
||||
.receiveRequest(.max(3)),
|
||||
.receiveRequest(.max(14)),
|
||||
.receiveRequest(.max(10)),
|
||||
.receiveRequest(.none)])
|
||||
|
||||
publisher.send(completion: .finished)
|
||||
publisher.send(completion: .failure(.oops))
|
||||
publisher.send(completion: .finished)
|
||||
XCTAssertEqual(publisher.send(144), .max(144))
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("HandleEvents"),
|
||||
.value(0),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3),
|
||||
.completion(.finished),
|
||||
.completion(.failure(.oops)),
|
||||
.completion(.finished),
|
||||
.value(144)])
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(14)),
|
||||
.requested(.max(10)),
|
||||
.requested(.none)])
|
||||
XCTAssertEqual(history, [.receiveSubscription("CustomSubscription"),
|
||||
.receiveOutput(0),
|
||||
.receiveOutput(1),
|
||||
.receiveRequest(.max(1)),
|
||||
.receiveOutput(2),
|
||||
.receiveRequest(.max(2)),
|
||||
.receiveOutput(3),
|
||||
.receiveRequest(.max(3)),
|
||||
.receiveRequest(.max(14)),
|
||||
.receiveRequest(.max(10)),
|
||||
.receiveRequest(.none),
|
||||
.receiveCompletion(.finished)])
|
||||
}
|
||||
|
||||
func testAccumulatesDemandUntilSubscriptionArrives() {
|
||||
var history = [Event<TestingError>]()
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let handleEvents = publisher.handleAllEvents { history.append($0) }
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: {
|
||||
XCTAssertNil(publisher.subscriber)
|
||||
XCTAssertEqual(history, [])
|
||||
XCTAssertEqual(subscription.history, [])
|
||||
$0.request(.max(45))
|
||||
$0.request(.none)
|
||||
$0.request(.max(13))
|
||||
XCTAssertEqual(subscription.history, [])
|
||||
downstreamSubscription = $0
|
||||
},
|
||||
receiveValue: { .max($0) }
|
||||
)
|
||||
handleEvents.subscribe(tracking)
|
||||
XCTAssertEqual(tracking.history, [.subscription("HandleEvents")])
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(58))])
|
||||
XCTAssertEqual(history, [.receiveRequest(.max(45)),
|
||||
.receiveRequest(.none),
|
||||
.receiveRequest(.max(13)),
|
||||
.receiveSubscription("CustomSubscription")])
|
||||
XCTAssertNotNil(downstreamSubscription)
|
||||
}
|
||||
|
||||
func testCancelAlreadyCancelled() throws {
|
||||
var history = [Event<TestingError>]()
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(2),
|
||||
receiveValueDemand: .max(5)) {
|
||||
$0.handleAllEvents { history.append($0) }
|
||||
}
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("HandleEvents")])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(2))])
|
||||
XCTAssertEqual(history, [.receiveRequest(.max(2)),
|
||||
.receiveSubscription("CustomSubscription")])
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(1))
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(1), .max(5))
|
||||
helper.publisher.send(completion: .finished)
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("HandleEvents"),
|
||||
.value(1),
|
||||
.completion(.finished),
|
||||
.completion(.failure(.oops))])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(2)),
|
||||
.cancelled])
|
||||
XCTAssertEqual(history, [.receiveRequest(.max(2)),
|
||||
.receiveSubscription("CustomSubscription"),
|
||||
.receiveCancel])
|
||||
}
|
||||
|
||||
func testHandleEventsReceiveSubscriptionTwice() throws {
|
||||
var history = [Event<TestingError>]()
|
||||
try testReceiveSubscriptionTwice { $0.handleAllEvents { history.append($0) } }
|
||||
XCTAssertEqual(history, [.receiveSubscription("CustomSubscription"),
|
||||
.receiveSubscription("CustomSubscription"),
|
||||
.receiveSubscription("CustomSubscription"),
|
||||
.receiveCancel])
|
||||
}
|
||||
|
||||
func testHandleEventsReceiveValueBeforeSubscription() {
|
||||
var history = [Event<Never>]()
|
||||
testReceiveValueBeforeSubscription(
|
||||
value: 144,
|
||||
expected: .history([.subscription("HandleEvents"),
|
||||
.value(144)],
|
||||
demand: .max(42)),
|
||||
{ $0.handleAllEvents { history.append($0) } }
|
||||
)
|
||||
XCTAssertEqual(history, [.receiveOutput(144),
|
||||
.receiveRequest(.max(42))])
|
||||
}
|
||||
|
||||
func testHandleEventsReceiveCompletionBeforeSubscription() {
|
||||
var history = [Event<Never>]()
|
||||
testReceiveCompletionBeforeSubscription(
|
||||
inputType: Int.self,
|
||||
expected: .history([.subscription("HandleEvents"), .completion(.finished)]),
|
||||
{ $0.handleAllEvents { history.append($0) } }
|
||||
)
|
||||
XCTAssertEqual(history, [.receiveCompletion(.finished)])
|
||||
}
|
||||
|
||||
func testHandleEventsRequestBeforeSubscription() {
|
||||
var history = [Event<Never>]()
|
||||
testRequestBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
{ $0.handleAllEvents { history.append($0) } })
|
||||
XCTAssertEqual(history, [.receiveRequest(.max(1))])
|
||||
}
|
||||
|
||||
func testHandleEventsCancelBeforeSubscription() {
|
||||
var history = [Event<Never>]()
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
{ $0.handleAllEvents { history.append($0) } })
|
||||
XCTAssertEqual(history, [.receiveCancel])
|
||||
}
|
||||
|
||||
func testHandleEventsReflection() throws {
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: Error.self,
|
||||
description: "HandleEvents",
|
||||
customMirror: childrenIsEmpty,
|
||||
playgroundDescription: "HandleEvents",
|
||||
{ $0.handleEvents() })
|
||||
}
|
||||
|
||||
func testHandleEventsLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
{ $0.handleEvents() })
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private enum Event<Failure: Error & Equatable>: Equatable {
|
||||
case receiveSubscription(StringSubscription)
|
||||
case receiveOutput(Int)
|
||||
case receiveCompletion(Subscribers.Completion<Failure>)
|
||||
case receiveCancel
|
||||
case receiveRequest(Subscribers.Demand)
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
extension Publisher where Output == Int, Failure: Equatable {
|
||||
fileprivate func handleAllEvents(
|
||||
_ handle: @escaping (Event<Failure>) -> Void
|
||||
) -> Publishers.HandleEvents<Self> {
|
||||
return handleEvents(
|
||||
receiveSubscription: { handle(.receiveSubscription(.subscription($0))) },
|
||||
receiveOutput: { handle(.receiveOutput($0)) },
|
||||
receiveCompletion: { handle(.receiveCompletion($0)) },
|
||||
receiveCancel: { handle(.receiveCancel) },
|
||||
receiveRequest: { handle(.receiveRequest($0)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user