20 Commits

Author SHA1 Message Date
Sergej Jaskiewicz 26e86a9905 Implement CombineLatest 2021-06-21 19:09:04 +03:00
Sergej Jaskiewicz bab8e08d2f Work around SwiftLint nested configuration bug
There is a bug introduced in SwiftLint 0.43.0 (?) when nested configurations don't work.
Nested configurations let us place additional .swiftlint.yml files in subdirectories that
specify rules that should only apply to that subdirectory. This is broken now.
2021-06-21 17:38:33 +03:00
Sergej Jaskiewicz 4060ee9f57 Fix compatibility with Xcode 12.5 toolchain and SDKs 2021-06-21 17:38:33 +03:00
Sergej Jaskiewicz 5996772433 Bump Xcode version for compatibility testing 2021-02-22 20:47:35 +03:00
Sergej Jaskiewicz cd45c77fac Implement Publishers.PrefixUntilOutput 2021-02-22 20:47:35 +03:00
Stuart Austin e618d179fe Add Publishers.Throttle implementation (#195)
* Publishers.Throttle implementation with tests

* Fix Throttle lint errors and removed expectation from throttle tests. Add additional test for cancelling a subscription before a scheduled value is emitted

* Fix VirtualTimeScheduler's executeSchedulesActions default deadline not being far enough into the future on 32-bit platforms.

* Fixed multiple lint errors

* Improve Publishers.Throttle code coverage by removing enum for pending emissions

* Additional Throttle test for cancelling a Subscriber when an output has been scheduled

* ThrottleTests now run on WASI
2021-02-18 13:56:55 +00:00
Marcus Scherer 4fa5f48c19 Fix typo (#204) 2021-02-08 19:41:49 +03:00
Max Desiatov 28993ae57d Add CHANGELOG.md, bump version to 0.12.0 (#202)
* Add CHANGELOG.md, bump version to 0.12.0

* Mention the timer bug fix in `CHANGELOG.md`
2021-01-29 15:05:56 +00:00
Grigory Entin 3d61bf87e7 Fixed Timer(timeInterval:,repeats:,block:) not accounting timeInterval for the first fire date. (#196)
https://github.com/OpenCombine/OpenCombine/blob/master/Sources/OpenCombineFoundation/Helpers/Portability.swift#L58-L64

It looks like this was a typo/something overlooked, but basically, this `fire: Date()` breaks at least every timer publisher like `Timer.publish(every: timeInterval, on: .main, in: .default)`, as it basically results in the *first* event fired immediately vs in timeInterval. (Just in case, no, Combine does not fire that extra event).

* Fixed Timer(timeInterval:,repeats:,block:) not accounting timeInterval for the first fire date.

* Fixed Danger warning about line length.
2021-01-29 13:42:17 +00:00
Max Desiatov 911a4e1aa3 Add OpenCombineShim product for easier importing (#197) 2021-01-25 17:25:28 +03:00
Yuta Saito beb38dec0e Implementation for ObservableObject with Mirror (#201)
A temporary implementation until we implement the proper type metadata introspection.
2021-01-25 17:24:19 +03:00
Nomo Nomad 1fbf688897 Update README.md (#199) 2020-12-11 16:41:20 +03:00
Sergej Jaskiewicz 5436868053 Fix some lock acquiring in Publishers.FlatMap (#194) 2020-11-08 17:44:33 +03:00
Sergej Jaskiewicz 4977ca158f Update DispatchQueue scheduler to match iOS 14.2 behavior 2020-11-07 17:28:08 +03:00
Sergej Jaskiewicz 96214ac5f9 Run compatibility tests on iOS 14.2 2020-11-07 17:28:08 +03:00
Sergej Jaskiewicz 21fda909f5 Implement Publishers.Retry 2020-11-07 17:28:08 +03:00
Sergej Jaskiewicz 8438d09b82 Increase time intervals in OperationQueue tests
The test is sporadically failing on iOS 9.3.
2020-11-03 17:21:34 +03:00
Sergej Jaskiewicz 30a60b52cc Add missing availability annotations in tests
Fixes #192
2020-11-03 17:21:34 +03:00
Sergej Jaskiewicz a93ed143fb Add more supported platforms to Package.swift 2020-11-03 17:21:34 +03:00
Max Desiatov e054a884ef Add support for SwiftWasm with CI and tests (#191)
WebAssembly support for atomics and multi-threading isn't fully standardized yet, and it not supported in SwiftWasm at the moment. Because of this Dispatch is unavailable, and all Combine-related Foundation stuff is unavailable too. Tests related to this are disabled. Locking functions are replaced with no-op shims.
2020-11-02 22:02:39 +00:00
92 changed files with 5110 additions and 709 deletions
+44 -15
View File
@@ -27,24 +27,24 @@ macOS_tests_steps: &macOS_tests_steps
name: Generating Xcode project
command: make generate-xcodeproj SWIFT_BUILD_FLAGS="-Xswiftc -warnings-as-errors"
- run:
name: Building for testing on macOS 10.15.0 with xcodebuild
name: Building for testing on macOS with xcodebuild
command: |
set -o pipefail \
&& xcodebuild build-for-testing \
-scheme OpenCombine-Package \
-sdk macosx10.15 \
-sdk macosx \
-derivedDataPath DerivedData \
| tee xcodebuild_build-for-testing.log \
| xcpretty
- store_artifacts:
path: xcodebuild_build-for-testing.log
- run:
name: Testing on macOS 10.15.0 with xcodebuild
name: Testing on macOS with xcodebuild
command: |
set -o pipefail \
&& xcodebuild test-without-building \
-scheme OpenCombine-Package \
-sdk macosx10.15 \
-sdk macosx \
-derivedDataPath DerivedData \
| tee xcodebuild_test-without-building.log \
| xcpretty --report junit -o build/reports/results.xml
@@ -113,36 +113,43 @@ jobs:
environment:
SWIFT_VERSION: "5.3.0"
<<: *macOS_tests_steps
"Execute compatibility tests on iOS 14.1 (Xcode 12.1.0, Swift 5.3.0)":
"Execute tests on macOS 11.4.0 (Xcode 12.5.0, Swift 5.4.0)":
macos:
xcode: "12.1.0"
xcode: "12.5.0"
environment:
SWIFT_VERSION: "5.3.0"
SWIFT_VERSION: "5.4.0"
<<: *macOS_tests_steps
"Execute compatibility tests on iOS 14.5 (Xcode 12.5.0, Swift 5.4.0)":
macos:
xcode: "12.5.0"
environment:
SWIFT_VERSION: "5.4.0"
steps:
- checkout
- run:
name: Generating Xcode project
command: make generate-compatibility-xcodeproj
- run:
name: Building for testing on iOS 14.1 with xcodebuild
name: Building for testing on iOS 14.5 with xcodebuild
command: |
set -o pipefail \
&& xcodebuild build-for-testing \
-scheme OpenCombine-Package \
-destination "platform=iOS Simulator,name=iPhone 11,OS=14.1" \
-destination "platform=iOS Simulator,name=iPhone 12,OS=14.5" \
-derivedDataPath DerivedData \
| tee xcodebuild_build-for-testing.log \
| xcpretty
- store_artifacts:
path: xcodebuild_build-for-testing.log
- run:
name: Testing against Combine on iOS 14.1 with xcodebuild
name: Testing against Combine on iOS 14.5 with xcodebuild
command: |
set -o pipefail \
&& xcodebuild test-without-building \
-scheme OpenCombine-Package \
-destination "platform=iOS Simulator,name=iPhone 11,OS=14.1" \
-destination "platform=iOS Simulator,name=iPhone 12,OS=14.5" \
-derivedDataPath DerivedData \
| tee xcodebuild_test-without-building.log \
| xcpretty --report junit -o build/reports/results.xml
@@ -151,6 +158,18 @@ jobs:
- store_test_results:
path: build/reports
"Execute compatibility tests on macOS 11.4.0 (Xcode 12.5.0, Swift 5.4.0)":
macos:
xcode: "12.5.0"
environment:
SWIFT_VERSION: "5.4.0"
steps:
- checkout
- run:
name: Testing against Combine on macOS 11.4.0
command: |
make test-compatibility
"Execute tests on iOS 9.3 (Xcode 10.2.1, Swift 5.0.1)":
macos:
xcode: "10.2.1"
@@ -239,10 +258,17 @@ jobs:
environment:
SWIFT_VERSION: "5.3"
<<: *ubuntu_tests_steps
"Execute tests on Ubuntu 18.04 (Swift 5.4)":
docker:
- image: swift:5.4-bionic
environment:
SWIFT_VERSION: "5.4"
<<: *ubuntu_tests_steps
"Run SwiftLint and Danger":
macos:
xcode: "11.3.0"
xcode: "11.4.0"
environment:
HOMEBREW_NO_AUTO_UPDATE: "1"
steps:
@@ -261,7 +287,7 @@ jobs:
"Run Pod spec lint":
macos:
xcode: "11.3.0"
xcode: "11.4.0"
environment:
HOMEBREW_NO_AUTO_UPDATE: "1"
steps:
@@ -277,9 +303,11 @@ workflows:
jobs:
- "Execute tests on macOS 10.15.0 (Xcode 11.3.0, Swift 5.1.3)"
- "Execute tests on macOS 10.15.0 (Xcode 12.1.0, Swift 5.3.0)"
- "Execute tests on macOS 11.4.0 (Xcode 12.5.0, Swift 5.4.0)"
"OpenCombine: execute compatibility tests":
jobs:
- "Execute compatibility tests on iOS 14.1 (Xcode 12.1.0, Swift 5.3.0)"
# - "Execute compatibility tests on iOS 14.5 (Xcode 12.5.0, Swift 5.4.0)"
- "Execute compatibility tests on macOS 11.4.0 (Xcode 12.5.0, Swift 5.4.0)"
"OpenCombine: execute tests on iOS":
jobs:
- "Execute tests on iOS 9.3 (Xcode 10.2.1, Swift 5.0.1)"
@@ -289,6 +317,7 @@ workflows:
- "Execute tests on Ubuntu 18.04 (Swift 5.1)"
- "Execute tests on Ubuntu 18.04 (Swift 5.2)"
- "Execute tests on Ubuntu 18.04 (Swift 5.3)"
- "Execute tests on Ubuntu 18.04 (Swift 5.4)"
"OpenCombine: run SwiftLint and Danger":
jobs:
- "Run SwiftLint and Danger"
+16
View File
@@ -0,0 +1,16 @@
name: SwiftWasm
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
carton_wasmer_test:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- uses: swiftwasm/swiftwasm-action@v5.3
+2
View File
@@ -2,6 +2,8 @@ included:
- Sources
- Tests
child_config: Tests/.swiftlint.yml
disabled_rules:
- block_based_kvo
- class_delegate_protocol
+299
View File
@@ -0,0 +1,299 @@
# 0.12.0 (29 Jan 2021)
This release adds a new `OpenCombineShim` product that will conditionally re-export either
Combine on Apple platforms, or OpenCombine on other platforms. Additionally, `ObservableObject`
protocol is now available and working on all platforms.
A bug with `Timer(timeInterval:repeats:block:)` firing immediately not accounting for the passed
`timeInterval` is fixed.
**Merged pull requests:**
- Fix `Timer(timeInterval:repeats:block:)` not accounting `timeInterval` ([#196](https://github.com/OpenCombine/OpenCombine/pull/196)) via [@grigorye](https://github.com/grigorye)
- Add `OpenCombineShim` product for easier importing ([#197](https://github.com/OpenCombine/OpenCombine/pull/197)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Implementation for `ObservableObject` with `Mirror` ([#201](https://github.com/OpenCombine/OpenCombine/pull/201)) via [@kateinoigakukun](https://github.com/kateinoigakukun)
# 0.11.0 (29 Oct 2020)
This release is compatible with Xcode 12.1.
### Additions
- `Publisher.assigned(to:)` method that accepts a `Published.Publisher`.
- New `Publisher.switchToLatest()` overloads.
- New `Publisher.flatMap(maxPublishers:_:)` overloads.
- `Optional.publisher` property.
- New `_Introspection` protocol that allows to track and explore the subscription graph and data flow.
### Bugfixes
- The project should now compile without warnings.
- The following entities have been updated to match the behavior of the newest Combine version:
- `Subscribers.Assign`
- `Publishers.Breakpoint`
- `Publishers.Buffer`
- `CombineIdentifier`
- `Publishers.CompactMap`
- `Publishers.Concatenate`
- `Publishers.Debounce`
- `Publishers.Delay`
- `DispatchQueue.SchedulerTimeType.Stride`
- `Publishers.Drop`
- `Publishers.Encode`
- `Publishers.Decode`
- `Publishers.Filter`
- `Publishers.HandleEvents`
- `Publishers.IgnoreOutput`
- `Publishers.MeasureInterval`
- `OperationQueue` scheduler
- `Published`
- `Publishers.ReceiveOn`
- `Publishers.ReplaceError`
- `RunLoop scheduler`
- `Publishers.Sequence`
- `Subscribers.Sink`
- `Publishers.SubscribeOn`
- `Publishers.Timeout`
- `Timer` publisher
### Known issues
- The default implementation of the `objectWillChange` requirement of the `ObservableObject` protocol is not available in Swift 5.1 and later.
# 0.10.2 (23 Oct 2020)
### Bugfixes
- Fixed a crash caused by recursive acquisition of a non-recursive lock in SubbjectSubscriber (#186, thanks @stuaustin for the bug report)
### Known issues
- The default implementation of the `objectWillChange` requirement of the `ObservableObject` protocol is not available in Swift 5.1 and later.
# 0.10.1 (4 Oct 2020)
### Bugfixes
- Fixed build errors on Linux with Swift 5.0 and Swift 5.3 toolchains (thanks, @adamleonard and @devmaximilian)
### Known issues
- The default implementation of the `objectWillChange` requirement of the `ObservableObject` protocol is not available in Swift 5.1 and later.
# 0.10.0 (28 Jun 2020)
This release is compatible with Xcode 11.5.
### Additions
- `Timer.publish(every:tolerance:on:in:options:)` (#156, thank you @MaxDesiatov)
- `OperationQueue` scheduler (#165)
- `Publishers.Timeout` (#164)
- `Publishers.Debounce` (#133)
### Bugfixes
- `PassthroughSubject`, `CurrentValueSubject` and `Future` have been rewritten from scratch. They are now faster, more correct and no longer leak subscriptions (#170).
### Known issues
- The default implementation of the `objectWillChange` requirement of the `ObservableObject` protocol is not available in Swift 5.1 and later.
# 0.9.0 (12 Jun 2020)
This release is compatible with Xcode 11.5.
### Additions
- The `Subscribers.Demand` struct can be nicely formatted in LLDB (#146, thank you @mayoff).
- `Publishers.SwitchToLatest` (#142).
- The `RunLoop` scheduler in `OpenCombineFoundation` (#131).
- `Publishers.Catch` and `Publishers.TryCatch` (#140).
### Bugfixes
- Worked around a [bug in the Swift compiler](https://bugs.swift.org/browse/SR-11680) when building the `COpenCombineHelpers` target (#145, thank you @mayoff).
- Improved documentation.
### Known issues
- The default implementation of the `objectWillChange` requirement of the `ObservableObject` protocol is not available in Swift 5.1 and later.
# 0.8.0 (17 Jan 2020)
This release is compatible with Xcode 11.3.1.
### Additions
- `Publishers.ReplaceEmpty` (#122, thank you @spadafiva)
- `NotificationCenter.Publisher` (#84)
- `URLSession.DataTaskPublisher` (#127)
- `Publishers.DropUntilOutput` (#136)
- `Publishers.CollectByCount` (#137)
- `Publishers.AssertNoFailure` (#138)
- `Publishers.Buffer` (#143)
### Bugfixes
- Fixed integer overflows in `DispatchQueue.SchedulerTimeType.Stride` (#126, #130)
- Fixed the 'default will never be executed' warning on non-Darwin platforms (like Linux) (#129)
### Known issues
- The default implementation of the `objectWillChange` requirement of the `ObservableObject` protocol is not available in Swift 5.1.
# 0.7.0 (10 Dec 2019)
This release is compatible with Xcode 11.2.1.
### Additions
- `Publishers.Delay` (#114)
- `Publishers.ReceiveOn` (#115)
- `Publishers.SubscribeOn` (#116)
- `Publishers.MeasureInterval` (#117)
- `Publishers.Breakpoint` (#118)
- `Publishers.HandleEvents` (#118)
- `Publishers.Concatenate` (#90)
### Known issues
- The default implementation of the `objectWillChange` requirement of the `ObservableObject` protocol is not available in Swift 5.1.
# 0.6.0 (26 Nov 2019)
This release is compatible with Xcode 11.2.1.
### Thread safety
- `Publishers.IgnoreOutput` has been audited for thread safety (#88)
- `Publishers.DropWhile` and `Publishers.TryDropWhile` have been audited for thread safety (#87)
### Additions
- `Publishers.Output` (#91)
- `Record` (#100)
- `Publishers.RemoveDuplicates`, `Publishers.TryRemoveDuplicates` (#89)
- `Publishers.PrefixWhile`, `Publishers.TryPrefixWhile` (#89)
- `Future` (#107, thanks @MaxDesiatov!)
### Bugfixes
- The behavior of the `Publishers.Encode` and `Publishers.Decode` subscriptions is fixed (#112)
- The behavior of the `Publishers.IgnoreOutput` subscription is fixed (#88)
- The behavior of the `Publishers.Print` subscription is fixed (#92)
- The behavior of the `Publishers.ReplaceError` subscription is fixed (#89)
- The behavior of the `Publishers.Filter` and `Publishers.TryFilter` subscriptions is fixed (#89)
- The behavior of the `Publishers.CompactMap` and `Publishers.TryCompactMap` subscriptions is fixed (#89)
- The behavior of the `Publishers.Multicast` subscription is fixed (#110)
- `Publishers.FlatMap` is reimplemented from scratch. Its behavior is fixed in many ways, it now fully matches that of Combine (#89)
- `@Published` property wrapper is fixed! (#112)
- The behavior of `DispatchQueue.SchedulerTimeType` is fixed to match that of the latest SDKs (#96)
- OpenCombine is now usable on 32 bit platforms. Why? Because we can.
### Known issues
- The default implementation of the `objectWillChange` requirement of the `ObservableObject` protocol is not available in Swift 5.1.
# 0.5.0 (17 Oct 2019)
This release is compatible with Xcode 11.1.
### Additions
- `Publishers.MapKeyPath` (#71)
- `Publishers.Reduce` (#76)
- `Publishers.TryReduce` (#76)
- `Publishers.Last` (#76)
- `Publishers.LastWhere` (#76)
- `Publishers.TryLastWhere` (#76)
- `Publishers.AllSatisfy` (#76)
- `Publishers.TryAllSatisfy` (#76)
- `Publishers.Contains` (#76)
- `Publishers.ContainsWhere` (#76)
- `Publishers.TryContainsWhere` (#76)
- `Publishers.Collect` (#76)
- `Publishers.Comparison` (#76)
- `Publishers.Drop` (#70, thank you @5sw!)
- `Publishers.Scan` (#83, thank you @epatey!)
- `Publishers.TryScan` (#83, thank you @epatey!)
### Bugfixes
- `Publishers.Print` doesn't print a redundant whitespace anymore.
### Known issues
- `@Published` property wrapper doesn't work yet
# 0.4.0 (8 Oct 2019)
This release is compatible with Xcode 11.1.
### Thread safety
- `SubjectSubscriber` (which is used when you subscribe a subject to a publisher) has been audited for thread-safety
- `Publishers.Multicast` has been audited for thread safety (#63)
- `Publishers.TryMap` has been audited for thread safety
- `Just` has been audited for thread safety
- `Optional.Publisher` has been audited for thread safety
- `Publishers.Sequence` has been audited for thread safety
- `Publishers.ReplaceError` has been audited for thread safety
- `Subscribers.Assign` has been audited for thread safety
- `Subscribers.Sink` has been audited for thread safety
### Bugfixes
- The semantics of `Publishers.Print`, `Publishers.TryMap` have been fixed
- Fix `iterator.next()` being called twice in `Publishers.Sequence` (#62)
- The default initializer of `CombineIdentifier` (the one that takes no arguments) is now much faster (#66, #69)
- When `Publishers.Sequence` subscription is cancelled while it emits values, the cancellation is respected (#73, thanks @5sw!)
### Additions
- `DispatchQueueScheduler` (#46)
- `Equatable` conformances for `First`, `ReplaceError`
- Added `eraseToAnyPublisher()` method (#59, thanks @evyasafhouzz for reporting!)
- `Publishers.MakeConnectable` (#61)
- `Publishers.Autoconnect` (#60)
- `Publishers.Share` (#60)
### Known issues
- `@Published` property wrapper doesn't work yet
# 0.3.0 (13 Sep 2019)
Among other things this release is compatible with Xcode 11.0 GM seed.
### Bugfixes
- Store newly send value in internal variable inside CurrentValueObject (#39, thanks @FranzBusch!)
### Additions
- `Filter`/`TryFilter` (#22, thanks @spadafiva!)
- `First`/`FirstWhere`/`TryFirstWhere` (#22, thanks again @spadafiva!)
- `CompactMap`/`TryCompacrMap` (#32)
- `IgnoreOutput` (#44, thanks @epatey!)
- `ReplaceError` (#50, thanks @vladiulianbogdan!)
- `FlatMap` (#45, thanks again @epatey!)
### Known issues
- `@Published` property wrapper doesn't work yet
# 0.2.0 (31 Jul 2019)
Updated for the newest Xcode 11.0 beta 5
# 0.1.0 (4 Jul 2019)
The first pre-pre-pre-alpha release is here!
Lots of stuff still unimplemented.
For now we have:
- `Just`
- `Publishers.Decode`
- `Publishers.DropWhile`
- `Publishers.Empty`
- `Publishers.Encode`
- `Publishers.Fail`
- `Publishers.Map`
- `Publishers.Multicast`
- `Publishers.Once`
- `Publishers.Optional`
- `Publishers.Print`
- `Publishers.Sequence`
- `Subscribers.Assign`
- `Subscribers.Completion`
- `Subscribers.Demand`
- `Subscribers.Sink`
- `AnyCancellable`
- `AnyPublisher`
- `AnySubject`
- `AnySubscriber`
- `Cancellable`
- `CombineIdentifier`
- `ConnectablePublisher`
- `CurrentValueSubject`
- `CustomCombineIdentifierConvertible`
- `ImmediateScheduler`
- `PassthroughSubject`
- `Publisher`
- `Result`
- `Scheduler`
- `Subject`
- `Subscriber`
- `Subscription`
+3 -3
View File
@@ -66,10 +66,10 @@ do {
}
}
SwiftLint.lint(inline: true,
SwiftLint.lint(.all(directory: nil),
inline: true,
configFile: ".swiftlint.yml",
strict: true,
lintAllFiles: true)
strict: true)
if danger.warnings.isEmpty, danger.fails.isEmpty {
markdown("LGTM")
+1 -1
View File
@@ -1,6 +1,6 @@
Pod::Spec.new do |spec|
spec.name = "OpenCombine"
spec.version = "0.11.0"
spec.version = "0.12.0"
spec.summary = "Open source implementation of Apple's Combine framework for processing values over time."
spec.description = <<-DESC
+1 -1
View File
@@ -1,6 +1,6 @@
Pod::Spec.new do |spec|
spec.name = "OpenCombineDispatch"
spec.version = "0.11.0"
spec.version = "0.12.0"
spec.summary = "OpenCombine + Dispatch interoperability"
spec.description = <<-DESC
+1 -1
View File
@@ -1,6 +1,6 @@
Pod::Spec.new do |spec|
spec.name = "OpenCombineFoundation"
spec.version = "0.11.0"
spec.version = "0.12.0"
spec.summary = "OpenCombine + OpenCombineFoundation interoperability"
spec.description = <<-DESC
+73 -9
View File
@@ -1,25 +1,89 @@
// swift-tools-version:5.0
// swift-tools-version:5.3
import PackageDescription
// This list should be updated whenever SwiftPM adds support for a new platform.
// See: https://bugs.swift.org/browse/SR-13814
let supportedPlatforms: [Platform] = [
.macOS,
.iOS,
.watchOS,
.tvOS,
.linux,
.android,
// Disable Windows because of https://bugs.swift.org/browse/SR-13817
// .windows,
.wasi,
]
let package = Package(
name: "OpenCombine",
products: [
.library(name: "OpenCombine", targets: ["OpenCombine"]),
.library(name: "OpenCombineDispatch", targets: ["OpenCombineDispatch"]),
.library(name: "OpenCombineFoundation", targets: ["OpenCombineFoundation"]),
.library(name: "OpenCombineShim", targets: ["OpenCombineShim"]),
],
targets: [
.target(name: "COpenCombineHelpers"),
.target(name: "OpenCombine", dependencies: ["COpenCombineHelpers"]),
.target(
name: "OpenCombine",
dependencies: [
.target(name: "COpenCombineHelpers",
condition: .when(platforms: supportedPlatforms.except([.wasi])))
],
exclude: [
"Publishers/Publishers.Encode.swift.gyb",
"Publishers/Publishers.MapKeyPath.swift.gyb",
"Publishers/Publishers.Catch.swift.gyb"
],
swiftSettings: [.define("WASI", .when(platforms: [.wasi]))]
),
.target(name: "OpenCombineDispatch", dependencies: ["OpenCombine"]),
.target(name: "OpenCombineFoundation", dependencies: ["OpenCombine",
"COpenCombineHelpers"]),
.testTarget(name: "OpenCombineTests",
dependencies: ["OpenCombine",
"OpenCombineDispatch",
"OpenCombineFoundation"],
swiftSettings: [.unsafeFlags(["-enable-testing"])])
.target(
name: "OpenCombineFoundation",
dependencies: [
"OpenCombine",
.target(name: "COpenCombineHelpers",
condition: .when(platforms: supportedPlatforms.except([.wasi])))
]
),
.target(
name: "OpenCombineShim",
dependencies: [
"OpenCombine",
.target(name: "OpenCombineDispatch",
condition: .when(platforms: supportedPlatforms.except([.wasi]))),
.target(name: "OpenCombineFoundation",
condition: .when(platforms: supportedPlatforms.except([.wasi])))
]
),
.testTarget(
name: "OpenCombineTests",
dependencies: [
"OpenCombine",
.target(name: "OpenCombineDispatch",
condition: .when(platforms: supportedPlatforms.except([.wasi]))),
.target(name: "OpenCombineFoundation",
condition: .when(platforms: supportedPlatforms.except([.wasi]))),
],
swiftSettings: [
.unsafeFlags(["-enable-testing"]),
.define("WASI", .when(platforms: [.wasi]))
]
)
],
cxxLanguageStandard: .cxx1z
)
// MARK: Helpers
extension Array where Element == Platform {
func except(_ exceptions: [Platform]) -> [Platform] {
// See: https://bugs.swift.org/browse/SR-13813
let exceptionsDescriptions = exceptions.map(String.init(describing:))
return filter { platform in
!exceptionsDescriptions.contains(String(describing: platform))
}
}
}
+34
View File
@@ -0,0 +1,34 @@
// swift-tools-version:5.0
import PackageDescription
let package = Package(
name: "OpenCombine",
products: [
.library(name: "OpenCombine", targets: ["OpenCombine"]),
.library(name: "OpenCombineDispatch", targets: ["OpenCombineDispatch"]),
.library(name: "OpenCombineFoundation", targets: ["OpenCombineFoundation"]),
.library(name: "OpenCombineShim", targets: ["OpenCombineShim"]),
],
targets: [
.target(name: "COpenCombineHelpers"),
.target(name: "OpenCombine", dependencies: ["COpenCombineHelpers"]),
.target(name: "OpenCombineDispatch", dependencies: ["OpenCombine"]),
.target(name: "OpenCombineFoundation", dependencies: ["OpenCombine",
"COpenCombineHelpers"]),
.target(
name: "OpenCombineShim",
dependencies: [
"OpenCombine",
"OpenCombineDispatch",
"OpenCombineFoundation",
]
),
.testTarget(name: "OpenCombineTests",
dependencies: ["OpenCombine",
"OpenCombineDispatch",
"OpenCombineFoundation"],
swiftSettings: [.unsafeFlags(["-enable-testing"])])
],
cxxLanguageStandard: .cxx1z
)
+34
View File
@@ -0,0 +1,34 @@
// swift-tools-version:5.1
import PackageDescription
let package = Package(
name: "OpenCombine",
products: [
.library(name: "OpenCombine", targets: ["OpenCombine"]),
.library(name: "OpenCombineDispatch", targets: ["OpenCombineDispatch"]),
.library(name: "OpenCombineFoundation", targets: ["OpenCombineFoundation"]),
.library(name: "OpenCombineShim", targets: ["OpenCombineShim"]),
],
targets: [
.target(name: "COpenCombineHelpers"),
.target(name: "OpenCombine", dependencies: ["COpenCombineHelpers"]),
.target(name: "OpenCombineDispatch", dependencies: ["OpenCombine"]),
.target(name: "OpenCombineFoundation", dependencies: ["OpenCombine",
"COpenCombineHelpers"]),
.target(
name: "OpenCombineShim",
dependencies: [
"OpenCombine",
"OpenCombineDispatch",
"OpenCombineFoundation",
]
),
.testTarget(name: "OpenCombineTests",
dependencies: ["OpenCombine",
"OpenCombineDispatch",
"OpenCombineFoundation"],
swiftSettings: [.unsafeFlags(["-enable-testing"])])
],
cxxLanguageStandard: .cxx1z
)
+34
View File
@@ -0,0 +1,34 @@
// swift-tools-version:5.2
import PackageDescription
let package = Package(
name: "OpenCombine",
products: [
.library(name: "OpenCombine", targets: ["OpenCombine"]),
.library(name: "OpenCombineDispatch", targets: ["OpenCombineDispatch"]),
.library(name: "OpenCombineFoundation", targets: ["OpenCombineFoundation"]),
.library(name: "OpenCombineShim", targets: ["OpenCombineShim"]),
],
targets: [
.target(name: "COpenCombineHelpers"),
.target(name: "OpenCombine", dependencies: ["COpenCombineHelpers"]),
.target(name: "OpenCombineDispatch", dependencies: ["OpenCombine"]),
.target(name: "OpenCombineFoundation", dependencies: ["OpenCombine",
"COpenCombineHelpers"]),
.target(
name: "OpenCombineShim",
dependencies: [
"OpenCombine",
"OpenCombineDispatch",
"OpenCombineFoundation",
]
),
.testTarget(name: "OpenCombineTests",
dependencies: ["OpenCombine",
"OpenCombineDispatch",
"OpenCombineFoundation"],
swiftSettings: [.unsafeFlags(["-enable-testing"])])
],
cxxLanguageStandard: .cxx1z
)
+22 -12
View File
@@ -2,36 +2,46 @@
[![OpenCombine](https://circleci.com/gh/OpenCombine/OpenCombine.svg?style=svg)](https://circleci.com/gh/OpenCombine/OpenCombine)
[![codecov](https://codecov.io/gh/OpenCombine/OpenCombine/branch/master/graph/badge.svg)](https://codecov.io/gh/OpenCombine/OpenCombine)
![Language](https://img.shields.io/badge/Swift-5.0-orange.svg)
![Platform](https://img.shields.io/badge/platform-Linux%20%7C%20macOS%20%7C%20iOS%20%7C%20watchOS%20%7C%20tvOS-lightgrey.svg)
![Platform](https://img.shields.io/badge/platform-Linux%20%7C%20macOS%20%7C%20iOS%20%7C%20watchOS%20%7C%20tvOS%20%7C%20Wasm-lightgrey.svg)
![Cocoapods](https://img.shields.io/cocoapods/v/OpenCombine?color=blue)
[<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.
The main goal of this project is to provide a compatible, reliable and efficient implementation which can be used on Apple's operating systems before macOS 10.15 and iOS 13, as well as Linux and Windows.
The main goal of this project is to provide a compatible, reliable and efficient implementation which can be used on Apple's operating systems before macOS 10.15 and iOS 13, as well as Linux and WebAssembly.
### Installation
`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`. The same applies to Foundation: if you want to use, for instance, `NotificationCenter` or `URLSession` publishers, you'll need to also import `OpenCombineFoundation`
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`.
If you develop code for multiple platforms, you may find it more convenient to import the
`OpenCombineShim` module instead. It conditionally re-exports Combine on Apple platforms (if
available), and all OpenCombine modules on other platforms. You can import `OpenCombineShim` only
when using SwiftPM. It is not currently available for CocoaPods.
##### Swift Package Manager
###### Swift Package
To add `OpenCombine` to your [SPM](https://swift.org/package-manager/) package, add the `OpenCombine` package to the list of package and target dependencies in your `Package.swift` file.
To add `OpenCombine` to your [SwiftPM](https://swift.org/package-manager/) package, add the `OpenCombine` package to the list of package and target dependencies in your `Package.swift` file. `OpenCombineDispatch` and `OpenCombineFoundation` products are currently not supported on WebAssembly. If your project targets WebAssembly exclusively, you should omit them from the list of your dependencies. If it targets multiple platforms including WebAssembly, depend on them only on non-WebAssembly platforms with [conditional target dependencies](https://github.com/apple/swift-evolution/blob/main/proposals/0273-swiftpm-conditional-target-dependencies.md).
```swift
dependencies: [
.package(url: "https://github.com/OpenCombine/OpenCombine.git", from: "0.11.0")
.package(url: "https://github.com/OpenCombine/OpenCombine.git", from: "0.12.0")
],
targets: [
.target(name: "MyAwesomePackage", dependencies: ["OpenCombine",
"OpenCombineDispatch",
"OpenCombineFoundation"])
.target(
name: "MyAwesomePackage",
dependencies: [
"OpenCombine",
.product(name: "OpenCombineFoundation", package: "OpenCombine"),
.product(name: "OpenCombineDispatch", package: "OpenCombine")
]
),
]
```
###### Xcode
`OpenCombine` can also be added as a SPM dependency directly in your Xcode project *(requires Xcode 11 upwards)*.
`OpenCombine` can also be added as a SwiftPM dependency directly in your Xcode project *(requires Xcode 11 upwards)*.
To do so, open Xcode, use **File****Swift Packages****Add Package Dependency…**, enter the [repository URL](https://github.com/OpenCombine/OpenCombine.git), choose the latest available version, and activate the checkboxes:
@@ -44,9 +54,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.11.0'
pod 'OpenCombineDispatch', '~> 0.11.0'
pod 'OpenCombineFoundation', '~> 0.11.0'
pod 'OpenCombine', '~> 0.12.0'
pod 'OpenCombineDispatch', '~> 0.12.0'
pod 'OpenCombineFoundation', '~> 0.12.0'
```
### Contributing
-391
View File
@@ -2,165 +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 and combines the latest elements from two publishers.
public struct CombineLatest<A, B> : Publisher where A : Publisher, B : Publisher, A.Failure == B.Failure {
/// The kind of values published by this publisher.
public typealias Output = (A.Output, B.Output)
/// The kind of errors this publisher might publish.
///
/// Use `Never` if this `Publisher` does not publish errors.
public typealias Failure = A.Failure
public let a: A
public let b: B
public init(_ a: A, _ b: B)
/// 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, B.Failure == S.Failure, S.Input == (A.Output, B.Output)
}
/// A publisher that receives and combines the latest elements from three publishers.
public struct CombineLatest3<A, B, C> : Publisher where A : Publisher, B : Publisher, C : Publisher, A.Failure == B.Failure, B.Failure == C.Failure {
/// The kind of values published by this publisher.
public typealias Output = (A.Output, B.Output, C.Output)
/// The kind of errors this publisher might publish.
///
/// Use `Never` if this `Publisher` does not publish errors.
public typealias Failure = A.Failure
public let a: A
public let b: B
public let c: C
public init(_ a: A, _ b: B, _ c: C)
/// 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, C.Failure == S.Failure, S.Input == (A.Output, B.Output, C.Output)
}
/// A publisher that receives and combines the latest elements from four publishers.
public struct CombineLatest4<A, B, C, D> : Publisher where A : Publisher, B : Publisher, C : Publisher, D : Publisher, A.Failure == B.Failure, B.Failure == C.Failure, C.Failure == D.Failure {
/// The kind of values published by this publisher.
public typealias Output = (A.Output, B.Output, C.Output, D.Output)
/// The kind of errors this publisher might publish.
///
/// Use `Never` if this `Publisher` does not publish errors.
public typealias Failure = A.Failure
public let a: A
public let b: B
public let c: C
public let d: D
public init(_ a: A, _ b: B, _ c: C, _ d: D)
/// 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, D.Failure == S.Failure, S.Input == (A.Output, B.Output, C.Output, D.Output)
}
}
extension Publisher {
/// Subscribes to an additional publisher and publishes a tuple upon receiving output from either publisher.
///
/// The combined publisher passes through any requests to *all* upstream publishers. However, it still obeys the demand-fulfilling rule of only sending the request amount downstream. If the demand isnt `.unlimited`, it drops values from upstream publishers. It implements this by using a buffer size of 1 for each upstream, and holds the most recent value in each buffer.
/// All upstream publishers need to finish for this publisher to finsh. If an upstream publisher never publishes a value, this publisher never finishes.
/// If any of the combined publishers terminates with a failure, this publisher also fails.
/// - Parameters:
/// - other: Another publisher to combine with this one.
/// - Returns: A publisher that receives and combines elements from this and another publisher.
public func combineLatest<P>(_ other: P) -> Publishers.CombineLatest<Self, P> where P : Publisher, Self.Failure == P.Failure
/// Subscribes to an additional publisher and invokes a closure upon receiving output from either publisher.
///
/// The combined publisher passes through any requests to *all* upstream publishers. However, it still obeys the demand-fulfilling rule of only sending the request amount downstream. If the demand isnt `.unlimited`, it drops values from upstream publishers. It implements this by using a buffer size of 1 for each upstream, and holds the most recent value in each buffer.
/// All upstream publishers need to finish for this publisher to finsh. If an upstream publisher never publishes a value, this publisher never finishes.
/// If any of the combined publishers terminates with a failure, this publisher also fails.
/// - Parameters:
/// - other: Another publisher to combine with this one.
/// - transform: A closure that receives the most recent value from each publisher and returns a new value to publish.
/// - Returns: A publisher that receives and combines elements from this and another publisher.
public func combineLatest<P, T>(_ other: P, _ transform: @escaping (Self.Output, P.Output) -> T) -> Publishers.Map<Publishers.CombineLatest<Self, P>, T> where P : Publisher, Self.Failure == P.Failure
/// Subscribes to two additional publishers and publishes a tuple upon receiving output from any of the publishers.
///
/// The combined publisher passes through any requests to *all* upstream publishers. However, it still obeys the demand-fulfilling rule of only sending the request amount downstream. If the demand isnt `.unlimited`, it drops values from upstream publishers. It implements this by using a buffer size of 1 for each upstream, and holds the most recent value in each buffer.
/// All upstream publishers need to finish for this publisher to finish. If an upstream publisher never publishes a value, this publisher never finishes.
/// If any of the combined publishers terminates with a failure, this publisher also fails.
/// - Parameters:
/// - publisher1: A second publisher to combine with this one.
/// - publisher2: A third publisher to combine with this one.
/// - Returns: A publisher that receives and combines elements from this publisher and two other publishers.
public func combineLatest<P, Q>(_ publisher1: P, _ publisher2: Q) -> Publishers.CombineLatest3<Self, P, Q> where P : Publisher, Q : Publisher, Self.Failure == P.Failure, P.Failure == Q.Failure
/// Subscribes to two additional publishers and invokes a closure upon receiving output from any of the publishers.
///
/// The combined publisher passes through any requests to *all* upstream publishers. However, it still obeys the demand-fulfilling rule of only sending the request amount downstream. If the demand isnt `.unlimited`, it drops values from upstream publishers. It implements this by using a buffer size of 1 for each upstream, and holds the most recent value in each buffer.
/// All upstream publishers need to finish for this publisher to finish. If an upstream publisher never publishes a value, this publisher never finishes.
/// If any of the combined publishers terminates with a failure, this publisher also fails.
/// - Parameters:
/// - publisher1: A second publisher to combine with this one.
/// - publisher2: A third publisher to combine with this one.
/// - transform: A closure that receives the most recent value from each publisher and returns a new value to publish.
/// - Returns: A publisher that receives and combines elements from this publisher and two other publishers.
public func combineLatest<P, Q, T>(_ publisher1: P, _ publisher2: Q, _ transform: @escaping (Self.Output, P.Output, Q.Output) -> T) -> Publishers.Map<Publishers.CombineLatest3<Self, P, Q>, T> where P : Publisher, Q : Publisher, Self.Failure == P.Failure, P.Failure == Q.Failure
/// Subscribes to three additional publishers and publishes a tuple upon receiving output from any of the publishers.
///
/// The combined publisher passes through any requests to *all* upstream publishers. However, it still obeys the demand-fulfilling rule of only sending the request amount downstream. If the demand isnt `.unlimited`, it drops values from upstream publishers. It implements this by using a buffer size of 1 for each upstream, and holds the most recent value in each buffer.
/// All upstream publishers need to finish for this publisher to finish. If an upstream publisher never publishes a value, this publisher never finishes.
/// If any of the combined publishers terminates with a failure, this publisher also fails.
/// - Parameters:
/// - publisher1: A second publisher to combine with this one.
/// - publisher2: A third publisher to combine with this one.
/// - publisher3: A fourth publisher to combine with this one.
/// - Returns: A publisher that receives and combines elements from this publisher and three other publishers.
public func combineLatest<P, Q, R>(_ publisher1: P, _ publisher2: Q, _ publisher3: R) -> Publishers.CombineLatest4<Self, P, Q, R> where P : Publisher, Q : Publisher, R : Publisher, Self.Failure == P.Failure, P.Failure == Q.Failure, Q.Failure == R.Failure
/// Subscribes to three additional publishers and invokes a closure upon receiving output from any of the publishers.
///
/// The combined publisher passes through any requests to *all* upstream publishers. However, it still obeys the demand-fulfilling rule of only sending the request amount downstream. If the demand isnt `.unlimited`, it drops values from upstream publishers. It implements this by using a buffer size of 1 for each upstream, and holds the most recent value in each buffer.
/// All upstream publishers need to finish for this publisher to finish. If an upstream publisher never publishes a value, this publisher never finishes.
/// If any of the combined publishers terminates with a failure, this publisher also fails.
/// - Parameters:
/// - publisher1: A second publisher to combine with this one.
/// - publisher2: A third publisher to combine with this one.
/// - publisher3: A fourth publisher to combine with this one.
/// - transform: A closure that receives the most recent value from each publisher and returns a new value to publish.
/// - Returns: A publisher that receives and combines elements from this publisher and three other publishers.
public func combineLatest<P, Q, R, T>(_ publisher1: P, _ publisher2: Q, _ publisher3: R, _ transform: @escaping (Self.Output, P.Output, Q.Output, R.Output) -> T) -> Publishers.Map<Publishers.CombineLatest4<Self, P, Q, R>, T> where P : Publisher, Q : Publisher, R : Publisher, Self.Failure == P.Failure, P.Failure == Q.Failure, Q.Failure == R.Failure
}
extension Publishers {
/// A strategy for collecting received elements.
@@ -244,47 +85,6 @@ extension Publisher {
public func collect<S>(_ strategy: Publishers.TimeGroupingStrategy<S>, options: S.SchedulerOptions? = nil) -> Publishers.CollectByTime<Self, S> where S : Scheduler
}
extension Publishers {
public struct PrefixUntilOutput<Upstream, Other> : Publisher where Upstream : Publisher, Other : 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
/// Another publisher, whose first output causes this publisher to finish.
public let other: Other
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.Failure == S.Failure, Upstream.Output == S.Input
}
}
extension Publisher {
/// Republishes elements until another publisher emits an element.
///
/// After the second publisher publishes an element, the publisher returned by this method finishes.
///
/// - Parameter publisher: A second publisher.
/// - Returns: A publisher that republishes elements until the second publisher publishes an element.
public func prefix<P>(untilOutputFrom publisher: P) -> Publishers.PrefixUntilOutput<Self, P> where P : Publisher
}
extension Publishers {
/// A publisher created by applying the merge function to two upstream publishers.
@@ -675,147 +475,6 @@ extension Publisher {
public func merge(with other: Self) -> Publishers.MergeMany<Self>
}
extension Publishers {
/// A publisher that attempts to recreate its subscription to a failed upstream publisher.
public struct Retry<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 retry attempts to perform.
///
/// If `nil`, this publisher attempts to reconnect with the upstream publisher an unlimited number of times.
public let retries: Int?
/// Creates a publisher that attempts to recreate its subscription to a failed upstream publisher.
///
/// - Parameters:
/// - upstream: The publisher from which this publisher receives its elements.
/// - retries: The maximum number of retry attempts to perform. If `nil`, this publisher attempts to reconnect with the upstream publisher an unlimited number of times.
public init(upstream: Upstream, retries: 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, Upstream.Output == S.Input
}
}
extension Publisher {
/// Attempts to recreate a failed subscription with the upstream publisher using a specified number of attempts to establish the connection.
///
/// After exceeding the specified number of retries, the publisher passes the failure to the downstream receiver.
/// - Parameter retries: The number of times to attempt to recreate the subscription.
/// - Returns: A publisher that attempts to recreate its subscription to a failed upstream publisher.
public func retry(_ retries: Int) -> Publishers.Retry<Self>
}
extension Publisher {
/// Attempts to recreate a failed subscription with the upstream publisher using a specified number of attempts to establish the connection.
///
/// After exceeding the specified number of retries, the publisher passes the failure to the downstream receiver.
/// - Parameter retries: The number of times to attempt to recreate the subscription.
/// - Returns: A publisher that attempts to recreate its subscription to a failed upstream publisher.
public func retry(_ retries: Int) -> Publishers.Retry<Self>
}
extension Publishers {
/// A publisher that publishes either the most-recent or first element published by the upstream publisher in a specified time interval.
public struct Throttle<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 interval in which to find and emit the most recent element.
public let interval: Context.SchedulerTimeType.Stride
/// The scheduler on which to publish elements.
public let scheduler: Context
/// A Boolean value indicating whether to publish the most recent element.
///
/// If `false`, the publisher emits the first element received during the interval.
public let latest: Bool
public init(upstream: Upstream, interval: Context.SchedulerTimeType.Stride, scheduler: Context, latest: Bool)
/// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)`
///
/// - SeeAlso: `subscribe(_:)`
/// - Parameters:
/// - subscriber: The subscriber to attach to this `Publisher`.
/// once attached it can begin to receive values.
public func receive<S>(subscriber: S) where S : Subscriber, Upstream.Failure == S.Failure, Upstream.Output == S.Input
}
}
extension Publisher {
/// Publishes either the most-recent or first element published by the upstream
/// publisher in the specified time interval.
///
/// Use `throttle(for:scheduler:latest:`` to selectively republish elements from
/// an upstream publisher during an interval you specify. Other elements received from
/// the upstream in the throttling interval arent republished.
///
/// In the example below, a `Timer.TimerPublisher` produces elements on 3-second
/// intervals; the `throttle(for:scheduler:latest:)` operator delivers the first
/// event, then republishes only the latest event in the following ten second
/// intervals:
///
/// cancellable = Timer.publish(every: 3.0, on: .main, in: .default)
/// .autoconnect()
/// .print("\(Date().description)")
/// .throttle(for: 10.0, scheduler: RunLoop.main, latest: true)
/// .sink(
/// receiveCompletion: { print ("Completion: \($0).") },
/// receiveValue: { print("Received Timestamp \($0).") }
/// )
///
/// // Prints:
/// // Publish at: 2020-03-19 18:26:54 +0000: receive value: (2020-03-19 18:26:57 +0000)
/// // Received Timestamp 2020-03-19 18:26:57 +0000.
/// // Publish at: 2020-03-19 18:26:54 +0000: receive value: (2020-03-19 18:27:00 +0000)
/// // Publish at: 2020-03-19 18:26:54 +0000: receive value: (2020-03-19 18:27:03 +0000)
/// // Publish at: 2020-03-19 18:26:54 +0000: receive value: (2020-03-19 18:27:06 +0000)
/// // Publish at: 2020-03-19 18:26:54 +0000: receive value: (2020-03-19 18:27:09 +0000)
/// // Received Timestamp 2020-03-19 18:27:09 +0000.
///
/// - Parameters:
/// - interval: The interval at which to find and emit either the most recent or
/// the first element, expressed in the time system of the scheduler.
/// - scheduler: The scheduler on which to publish elements.
/// - latest: A Boolean value that indicates whether to publish the most recent
/// element. If `false`, the publisher emits the first element received during
/// the interval.
/// - Returns: A publisher that emits either the most-recent or first element received
/// during the specified interval.
public func throttle<S>(for interval: S.SchedulerTimeType.Stride, scheduler: S, latest: Bool) -> Publishers.Throttle<Self, S> where S : Scheduler
}
extension Publishers {
/// A publisher created by applying the zip function to two upstream publishers.
@@ -979,43 +638,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.CombineLatest : Equatable where A : Equatable, B : Equatable {
/// Returns a Boolean value that indicates whether two publishers are equivalent.
///
/// - Parameters:
/// - lhs: A combineLatest publisher to compare for equality.
/// - rhs: Another combineLatest publisher to compare for equality.
/// - Returns: `true` if the corresponding upstream publishers of each combineLatest publisher are equal, `false` otherwise.
public static func == (lhs: Publishers.CombineLatest<A, B>, rhs: Publishers.CombineLatest<A, B>) -> Bool
}
extension Publishers.CombineLatest3 : Equatable where A : Equatable, B : Equatable, C : 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.CombineLatest3<A, B, C>, rhs: Publishers.CombineLatest3<A, B, C>) -> Bool
}
extension Publishers.CombineLatest4 : Equatable where A : Equatable, B : Equatable, C : Equatable, D : 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.CombineLatest4<A, B, C, D>, rhs: Publishers.CombineLatest4<A, B, C, D>) -> Bool
}
extension Publishers.Merge : Equatable where A : Equatable, B : Equatable {
/// Returns a Boolean value that indicates whether two publishers are equivalent.
@@ -1106,19 +728,6 @@ extension Publishers.MergeMany : Equatable where Upstream : Equatable {
public static func == (lhs: Publishers.MergeMany<Upstream>, rhs: Publishers.MergeMany<Upstream>) -> Bool
}
extension Publishers.Retry : 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.Retry<Upstream>, rhs: Publishers.Retry<Upstream>) -> Bool
}
extension Publishers.Zip : Equatable where A : Equatable, B : Equatable {
/// Returns a Boolean value that indicates whether two publishers are equivalent.
+1 -1
View File
@@ -9,7 +9,7 @@ extension Publisher {
/// Wraps this publisher with a type eraser.
///
/// Use `eraseToAnyPublisher()` to expose an instance of `AnyPublishe`` to
/// Use `eraseToAnyPublisher()` to expose an instance of `AnyPublisher`` to
/// the downstream subscriber, rather than this publishers actual type.
/// This form of _type erasure_ preserves abstraction across API boundaries, such as
/// different modules.
@@ -9,6 +9,15 @@
import COpenCombineHelpers
#endif
#if WASI
private var __identifier: UInt64 = 0
internal func __nextCombineIdentifier() -> UInt64 {
defer { __identifier += 1 }
return __identifier
}
#endif // WASI
/// A unique identifier for identifying publisher streams.
///
/// To conform to `CustomCombineIdentifierConvertible` in a
@@ -0,0 +1,205 @@
//
// AbstractCombineLatest.swift
//
//
// Created by Sergej Jaskiewicz on 10.12.2019.
//
internal class AbstractCombineLatest<Output, Failure, Downstream: Subscriber>
where Downstream.Input == Output, Downstream.Failure == Failure
{
private let downstream: Downstream
// TODO: The size of these arrays always stays the same.
// Maybe we can leverage ManagedBuffer/ManagedBufferPointer here
// to avoid additional allocations.
private var buffers: [Any?] // 0x78
private var subscriptions: [Subscription?] // 0x80
private var demand = Subscribers.Demand.none // 0x88
private var recursion = false // 0x90
private var finished = false // 0x98
private var errored = false // 0xA0
private var cancelled = false // 0xA8
private let upstreamCount: Int // 0xB0
private var finishCount = 0 // 0xB8
private let lock = UnfairLock.allocate() // 0xC0
private let downstreamLock = UnfairRecursiveLock.allocate() // 0xC8
internal init(downstream: Downstream, upstreamCount: Int) {
self.downstream = downstream
self.buffers = Array(repeating: nil, count: upstreamCount)
self.subscriptions = Array(repeating: nil, count: upstreamCount)
self.upstreamCount = upstreamCount
}
deinit {
lock.deallocate()
downstreamLock.deallocate()
}
// TODO: There should be more type-safe (and faster) way.
// E. g. what if we store `buffers` in subclasses?
internal func convert(values: [Any?]) -> Output {
abstractMethod()
}
fileprivate final func receive(subscription: Subscription, index: Int) {
lock.lock()
guard !cancelled && subscriptions[index] == nil else {
lock.unlock()
subscription.cancel()
return
}
subscriptions[index] = subscription
lock.unlock()
}
fileprivate final func receive(_ input: Any, index: Int) -> Subscribers.Demand {
lock.lock()
if cancelled || finished {
lock.unlock()
return .none
}
buffers[index] = input
guard !recursion && demand > 0 && buffers.allSatisfy({ $0 != nil }) else {
lock.unlock()
return .none
}
demand -= 1
recursion = true
lock.unlock()
downstreamLock.lock()
let newDemand = downstream.receive(convert(values: buffers))
downstreamLock.unlock()
lock.lock()
recursion = false
demand += newDemand
lock.unlock()
return .none
}
fileprivate final func receive(completion: Subscribers.Completion<Failure>,
index: Int) {
switch completion {
case .finished:
lock.lock()
if finished {
lock.unlock()
return
}
finishCount += 1
subscriptions[index] = nil
if finishCount == upstreamCount {
finished = true
buffers = Array(repeating: nil, count: upstreamCount)
lock.unlock()
downstreamLock.lock()
downstream.receive(completion: completion)
downstreamLock.unlock()
} else {
lock.unlock()
}
case .failure:
lock.lock()
finished = true
errored = true
let subscriptions = self.subscriptions
self.subscriptions = Array(repeating: nil, count: upstreamCount)
buffers = Array(repeating: nil, count: upstreamCount)
lock.unlock()
for (i, subscription) in subscriptions.enumerated() where i != index {
subscription?.cancel()
}
downstreamLock.lock()
downstream.receive(completion: completion)
downstreamLock.unlock()
}
}
}
extension AbstractCombineLatest: Subscription {
internal func request(_ demand: Subscribers.Demand) {
demand.assertNonZero() // TODO: Test this
lock.lock()
guard !cancelled && !finished else {
lock.unlock()
return
}
self.demand += demand
lock.unlock()
for subscription in subscriptions {
subscription?.request(demand)
}
}
internal func cancel() {
lock.lock()
cancelled = true
let subscriptions = self.subscriptions
self.subscriptions = Array(repeating: nil, count: upstreamCount)
buffers = Array(repeating: nil, count: upstreamCount)
lock.unlock()
for subscription in subscriptions {
subscription?.cancel()
}
}
}
extension AbstractCombineLatest: CustomStringConvertible {
internal var description: String { return "CombineLatest" }
}
extension AbstractCombineLatest: CustomReflectable {
internal var customMirror: Mirror {
lock.lock()
defer { lock.unlock() }
let children: [Mirror.Child] = [
("downstream", downstream),
("upstreamSubscriptions", subscriptions),
("demand", demand),
("buffers", buffers)
]
return Mirror(self, children: children)
}
}
extension AbstractCombineLatest: CustomPlaygroundDisplayConvertible {
internal final var playgroundDescription: Any { return description }
}
extension AbstractCombineLatest {
internal struct Side<Input>: Subscriber, CustomStringConvertible {
private let index: Int
private let combiner: AbstractCombineLatest
internal let combineIdentifier = CombineIdentifier()
internal init(index: Int, combiner: AbstractCombineLatest) {
self.index = index
self.combiner = combiner
}
internal func receive(subscription: Subscription) {
combiner.receive(subscription: subscription, index: index)
}
internal func receive(_ input: Input) -> Subscribers.Demand {
return combiner.receive(input, index: index)
}
internal func receive(completion: Subscribers.Completion<Failure>) {
combiner.receive(completion: completion, index: index)
}
internal var description: String { return "CombineLatest" }
}
}
+17
View File
@@ -9,5 +9,22 @@
import COpenCombineHelpers
#endif
#if WASI
internal struct __UnfairLock { // swiftlint:disable:this type_name
internal static func allocate() -> UnfairLock { return .init() }
internal func lock() {}
internal func unlock() {}
internal func assertOwner() {}
internal func deallocate() {}
}
internal struct __UnfairRecursiveLock { // swiftlint:disable:this type_name
internal static func allocate() -> UnfairRecursiveLock { return .init() }
internal func lock() {}
internal func unlock() {}
internal func deallocate() {}
}
#endif // WASI
internal typealias UnfairLock = __UnfairLock
internal typealias UnfairRecursiveLock = __UnfairRecursiveLock
@@ -48,8 +48,6 @@ internal class ReduceProducer<Downstream: Subscriber,
private var upstreamCompleted = false
private var empty = true
internal init(downstream: Downstream, initial: Output?, reduce: Reducer) {
self.downstream = downstream
self.initial = initial
@@ -100,7 +98,9 @@ internal class ReduceProducer<Downstream: Subscriber,
return
}
upstreamCompleted = true
self.completed = downstreamRequested || empty
if downstreamRequested {
self.completed = true
}
let completed = self.completed
let result = self.result
lock.unlock()
@@ -157,7 +157,6 @@ extension ReduceProducer: Subscriber {
lock.unlock()
return .none
}
empty = false
lock.unlock()
// Combine doesn't hold the lock when calling `receive(newValue:)`.
+50 -18
View File
@@ -42,26 +42,58 @@ public protocol ObservableObject: AnyObject {
var objectWillChange: ObjectWillChangePublisher { get }
}
extension ObservableObject where ObjectWillChangePublisher == ObservableObjectPublisher {
// swiftlint:disable let_var_whitespace
#if swift(>=5.1)
/// A publisher that emits before the object has changed.
@available(*, unavailable, message: """
The default implementation of objectWillChange is not available yet. \
It's being worked on in \
https://github.com/broadwaylamb/OpenCombine/pull/97
""")
public var objectWillChange: ObservableObjectPublisher {
fatalError("unimplemented")
}
#else
public var objectWillChange: ObservableObjectPublisher {
return ObservableObjectPublisher()
}
#endif
// swiftlint:enable let_var_whitespace
private protocol _ObservableObjectProperty {
var objectWillChange: ObservableObjectPublisher? { get nonmutating set }
}
#if swift(>=5.1)
extension Published: _ObservableObjectProperty {}
extension ObservableObject where ObjectWillChangePublisher == ObservableObjectPublisher {
/// A publisher that emits before the object has changed.
public var objectWillChange: ObservableObjectPublisher {
var installedPublisher: ObservableObjectPublisher?
var reflection: Mirror? = Mirror(reflecting: self)
while let aClass = reflection {
for (_, property) in aClass.children {
guard let property = property as? _ObservableObjectProperty else {
// Visit other fields until we meet a @Published field
continue
}
// Now we know that the field is @Published.
if let alreadyInstalledPublisher = property.objectWillChange {
installedPublisher = alreadyInstalledPublisher
// Don't visit other fields, as all @Published fields
// already have a publisher installed.
break
}
// Okay, this field doesn't have a publisher installed.
// This means that other fields don't have it either
// (because we install it only once and fields can't be added at runtime).
var lazilyCreatedPublisher: ObjectWillChangePublisher {
if let publisher = installedPublisher {
return publisher
}
let publisher = ObservableObjectPublisher()
installedPublisher = publisher
return publisher
}
property.objectWillChange = lazilyCreatedPublisher
// Continue visiting other fields.
}
reflection = aClass.superclassMirror
}
return installedPublisher ?? ObservableObjectPublisher()
}
}
#endif
/// A publisher that publishes changes from observable objects.
public final class ObservableObjectPublisher: Publisher {
+24 -12
View File
@@ -107,8 +107,16 @@ public struct Published<Value> {
case value(Value)
case publisher(Publisher)
}
@propertyWrapper
private final class Box {
var wrappedValue: Storage
private var storage: Storage
init(wrappedValue: Storage) {
self.wrappedValue = wrappedValue
}
}
@Box private var storage: Storage
internal var objectWillChange: ObservableObjectPublisher? {
get {
@@ -119,8 +127,8 @@ public struct Published<Value> {
return publisher.subject.objectWillChange
}
}
set {
projectedValue.subject.objectWillChange = newValue
nonmutating set {
getPublisher().subject.objectWillChange = newValue
}
}
@@ -145,7 +153,7 @@ public struct Published<Value> {
///
/// - Parameter initialValue: The publisher's initial value.
public init(wrappedValue: Value) {
storage = .value(wrappedValue)
_storage = Box(wrappedValue: .value(wrappedValue))
}
/// The property for which this instance exposes a publisher.
@@ -153,14 +161,7 @@ public struct Published<Value> {
/// The `projectedValue` is the property accessed with the `$` operator.
public var projectedValue: Publisher {
mutating get {
switch storage {
case .value(let value):
let publisher = Publisher(value)
storage = .publisher(publisher)
return publisher
case .publisher(let publisher):
return publisher
}
return getPublisher()
}
set { // swiftlint:disable:this unused_setter_value
switch storage {
@@ -172,6 +173,17 @@ public struct Published<Value> {
}
}
/// Note: This method can mutate `storage`
internal func getPublisher() -> Publisher {
switch storage {
case .value(let value):
let publisher = Publisher(value)
storage = .publisher(publisher)
return publisher
case .publisher(let publisher):
return publisher
}
}
// swiftlint:disable let_var_whitespace
@available(*, unavailable, message: """
@Published is only available on properties of classes
@@ -0,0 +1,408 @@
//
//
// Auto-generated from GYB template. DO NOT EDIT!
//
//
//
//
// Publishers.CombineLatest.swift.gyb
//
//
// Created by Sergej Jaskiewicz on 10.12.2019.
//
// swiftlint:disable generic_type_name
// swiftlint:disable large_tuple
// MARK: - CombineLatest methods on Publisher
extension Publisher {
/// Subscribes to an additional publisher and publishes a tuple upon
/// receiving output from either publisher.
///
/// The combined publisher passes through any requests to *all* upstream publishers.
/// However, it still obeys the demand-fulfilling rule of only sending the request
/// amount downstream. If the demand isnt `.unlimited`, it drops values from upstream
/// publishers. It implements this by using a buffer size of 1 for each upstream, and
/// holds the most recent value in each buffer.
/// All upstream publishers need to finish for this publisher to finsh. If an upstream
/// publisher never publishes a value, this publisher never finishes.
/// If any of the combined publishers terminates with a failure, this publisher also
/// fails.
///
/// - Parameters:
/// - other: Another publisher to combine with this one.
/// - Returns: A publisher that receives and combines elements from this and another
/// publisher.
public func combineLatest<P: Publisher>(
_ other: P
) -> Publishers.CombineLatest<Self, P>
where Failure == P.Failure
{
return .init(self, other)
}
/// Subscribes to an additional publisher and invokes a closure
/// upon receiving output from either publisher.
///
/// The combined publisher passes through any requests to *all* upstream publishers.
/// However, it still obeys the demand-fulfilling rule of only sending the request
/// amount downstream. If the demand isnt `.unlimited`, it drops values from upstream
/// publishers. It implements this by using a buffer size of 1 for each upstream, and
/// holds the most recent value in each buffer.
/// All upstream publishers need to finish for this publisher to finsh. If an upstream
/// publisher never publishes a value, this publisher never finishes.
/// If any of the combined publishers terminates with a failure, this publisher also
/// fails.
///
/// - Parameters:
/// - other: Another publisher to combine with this one.
/// - transform: A closure that receives the most recent value from each publisher
/// and returns a new value to publish.
/// - Returns: A publisher that receives and combines elements from this and another
/// publisher.
public func combineLatest<P: Publisher, Result>(
_ other: P,
_ transform: @escaping (Output, P.Output) -> Result
) -> Publishers.Map<Publishers.CombineLatest<Self, P>, Result>
where Failure == P.Failure
{
return Publishers.CombineLatest(self, other).map {
transform($0, $1)
}
}
/// Subscribes to two additional publishers and publishes a tuple upon
/// receiving output from either publisher.
///
/// The combined publisher passes through any requests to *all* upstream publishers.
/// However, it still obeys the demand-fulfilling rule of only sending the request
/// amount downstream. If the demand isnt `.unlimited`, it drops values from upstream
/// publishers. It implements this by using a buffer size of 1 for each upstream, and
/// holds the most recent value in each buffer.
/// All upstream publishers need to finish for this publisher to finsh. If an upstream
/// publisher never publishes a value, this publisher never finishes.
/// If any of the combined publishers terminates with a failure, this publisher also
/// fails.
///
/// - Parameters:
/// - publisher1: A second publisher to combine with this one.
/// - publisher2: A third publisher to combine with this one.
/// - Returns: A publisher that receives and combines elements from this and another
/// publisher.
public func combineLatest<P: Publisher, Q: Publisher>(
_ publisher1: P,
_ publisher2: Q
) -> Publishers.CombineLatest3<Self, P, Q>
where Failure == P.Failure,
P.Failure == Q.Failure
{
return .init(self, publisher1, publisher2)
}
/// Subscribes to two additional publishers and invokes a closure
/// upon receiving output from either publisher.
///
/// The combined publisher passes through any requests to *all* upstream publishers.
/// However, it still obeys the demand-fulfilling rule of only sending the request
/// amount downstream. If the demand isnt `.unlimited`, it drops values from upstream
/// publishers. It implements this by using a buffer size of 1 for each upstream, and
/// holds the most recent value in each buffer.
/// All upstream publishers need to finish for this publisher to finsh. If an upstream
/// publisher never publishes a value, this publisher never finishes.
/// If any of the combined publishers terminates with a failure, this publisher also
/// fails.
///
/// - Parameters:
/// - publisher1: A second publisher to combine with this one.
/// - publisher2: A third publisher to combine with this one.
/// - transform: A closure that receives the most recent value from each publisher
/// and returns a new value to publish.
/// - Returns: A publisher that receives and combines elements from this and another
/// publisher.
public func combineLatest<P: Publisher, Q: Publisher, Result>(
_ publisher1: P,
_ publisher2: Q,
_ transform: @escaping (Output, P.Output, Q.Output) -> Result
) -> Publishers.Map<Publishers.CombineLatest3<Self, P, Q>, Result>
where Failure == P.Failure,
P.Failure == Q.Failure
{
return Publishers.CombineLatest3(self, publisher1, publisher2).map {
transform($0, $1, $2)
}
}
/// Subscribes to three additional publishers and publishes a tuple upon
/// receiving output from either publisher.
///
/// The combined publisher passes through any requests to *all* upstream publishers.
/// However, it still obeys the demand-fulfilling rule of only sending the request
/// amount downstream. If the demand isnt `.unlimited`, it drops values from upstream
/// publishers. It implements this by using a buffer size of 1 for each upstream, and
/// holds the most recent value in each buffer.
/// All upstream publishers need to finish for this publisher to finsh. If an upstream
/// publisher never publishes a value, this publisher never finishes.
/// If any of the combined publishers terminates with a failure, this publisher also
/// fails.
///
/// - Parameters:
/// - publisher1: A second publisher to combine with this one.
/// - publisher2: A third publisher to combine with this one.
/// - publisher3: A fourth publisher to combine with this one.
/// - Returns: A publisher that receives and combines elements from this and another
/// publisher.
public func combineLatest<P: Publisher, Q: Publisher, R: Publisher>(
_ publisher1: P,
_ publisher2: Q,
_ publisher3: R
) -> Publishers.CombineLatest4<Self, P, Q, R>
where Failure == P.Failure,
P.Failure == Q.Failure,
Q.Failure == R.Failure
{
return .init(self, publisher1, publisher2, publisher3)
}
/// Subscribes to three additional publishers and invokes a closure
/// upon receiving output from either publisher.
///
/// The combined publisher passes through any requests to *all* upstream publishers.
/// However, it still obeys the demand-fulfilling rule of only sending the request
/// amount downstream. If the demand isnt `.unlimited`, it drops values from upstream
/// publishers. It implements this by using a buffer size of 1 for each upstream, and
/// holds the most recent value in each buffer.
/// All upstream publishers need to finish for this publisher to finsh. If an upstream
/// publisher never publishes a value, this publisher never finishes.
/// If any of the combined publishers terminates with a failure, this publisher also
/// fails.
///
/// - Parameters:
/// - publisher1: A second publisher to combine with this one.
/// - publisher2: A third publisher to combine with this one.
/// - publisher3: A fourth publisher to combine with this one.
/// - transform: A closure that receives the most recent value from each publisher
/// and returns a new value to publish.
/// - Returns: A publisher that receives and combines elements from this and another
/// publisher.
public func combineLatest<P: Publisher, Q: Publisher, R: Publisher, Result>(
_ publisher1: P,
_ publisher2: Q,
_ publisher3: R,
_ transform: @escaping (Output, P.Output, Q.Output, R.Output) -> Result
) -> Publishers.Map<Publishers.CombineLatest4<Self, P, Q, R>, Result>
where Failure == P.Failure,
P.Failure == Q.Failure,
Q.Failure == R.Failure
{
return Publishers.CombineLatest4(self, publisher1, publisher2, publisher3).map {
transform($0, $1, $2, $3)
}
}
}
// MARK: - CombineLatest publishers
extension Publishers {
/// A publisher that receives and combines the latest elements from two
/// publishers.
public struct CombineLatest<A: Publisher, B: Publisher>
: Publisher
where A.Failure == B.Failure
{
public typealias Output = (A.Output, B.Output)
public typealias Failure = A.Failure
public let a: A
public let b: B
public init(
_ a: A,
_ b: B
) {
self.a = a
self.b = b
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Downstream.Failure == Failure,
Downstream.Input == Output
{
typealias Inner = CombineLatest2Inner<A.Output,
B.Output,
Failure,
Downstream>
let inner = Inner(downstream: subscriber, upstreamCount: 2)
a.subscribe(Inner.Side(index: 0, combiner: inner))
b.subscribe(Inner.Side(index: 1, combiner: inner))
subscriber.receive(subscription: inner)
}
}
/// A publisher that receives and combines the latest elements from three
/// publishers.
public struct CombineLatest3<A: Publisher, B: Publisher, C: Publisher>
: Publisher
where A.Failure == B.Failure,
B.Failure == C.Failure
{
public typealias Output = (A.Output, B.Output, C.Output)
public typealias Failure = A.Failure
public let a: A
public let b: B
public let c: C
public init(
_ a: A,
_ b: B,
_ c: C
) {
self.a = a
self.b = b
self.c = c
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Downstream.Failure == Failure,
Downstream.Input == Output
{
typealias Inner = CombineLatest3Inner<A.Output,
B.Output,
C.Output,
Failure,
Downstream>
let inner = Inner(downstream: subscriber, upstreamCount: 3)
a.subscribe(Inner.Side(index: 0, combiner: inner))
b.subscribe(Inner.Side(index: 1, combiner: inner))
c.subscribe(Inner.Side(index: 2, combiner: inner))
subscriber.receive(subscription: inner)
}
}
/// A publisher that receives and combines the latest elements from four
/// publishers.
public struct CombineLatest4<A: Publisher, B: Publisher, C: Publisher, D: Publisher>
: Publisher
where A.Failure == B.Failure,
B.Failure == C.Failure,
C.Failure == D.Failure
{
public typealias Output = (A.Output, B.Output, C.Output, D.Output)
public typealias Failure = A.Failure
public let a: A
public let b: B
public let c: C
public let d: D
public init(
_ a: A,
_ b: B,
_ c: C,
_ d: D
) {
self.a = a
self.b = b
self.c = c
self.d = d
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Downstream.Failure == Failure,
Downstream.Input == Output
{
typealias Inner = CombineLatest4Inner<A.Output,
B.Output,
C.Output,
D.Output,
Failure,
Downstream>
let inner = Inner(downstream: subscriber, upstreamCount: 4)
a.subscribe(Inner.Side(index: 0, combiner: inner))
b.subscribe(Inner.Side(index: 1, combiner: inner))
c.subscribe(Inner.Side(index: 2, combiner: inner))
d.subscribe(Inner.Side(index: 3, combiner: inner))
subscriber.receive(subscription: inner)
}
}
}
// MARK: - Equatable conformances
extension Publishers.CombineLatest: Equatable
where
A: Equatable,
B: Equatable {}
extension Publishers.CombineLatest3: Equatable
where
A: Equatable,
B: Equatable,
C: Equatable {}
extension Publishers.CombineLatest4: Equatable
where
A: Equatable,
B: Equatable,
C: Equatable,
D: Equatable {}
// MARK: - Inners
private final class CombineLatest2Inner<Input0,
Input1,
Failure,
Downstream: Subscriber>
: AbstractCombineLatest<(Input0, Input1), Failure, Downstream>
where Downstream.Input == (Input0, Input1),
Downstream.Failure == Failure
{
override func convert(values: [Any?]) -> (Input0, Input1) {
return (values[0] as! Input0,
values[1] as! Input1)
}
}
private final class CombineLatest3Inner<Input0,
Input1,
Input2,
Failure,
Downstream: Subscriber>
: AbstractCombineLatest<(Input0, Input1, Input2), Failure, Downstream>
where Downstream.Input == (Input0, Input1, Input2),
Downstream.Failure == Failure
{
override func convert(values: [Any?]) -> (Input0, Input1, Input2) {
return (values[0] as! Input0,
values[1] as! Input1,
values[2] as! Input2)
}
}
private final class CombineLatest4Inner<Input0,
Input1,
Input2,
Input3,
Failure,
Downstream: Subscriber>
: AbstractCombineLatest<(Input0, Input1, Input2, Input3), Failure, Downstream>
where Downstream.Input == (Input0, Input1, Input2, Input3),
Downstream.Failure == Failure
{
override func convert(values: [Any?]) -> (Input0, Input1, Input2, Input3) {
return (values[0] as! Input0,
values[1] as! Input1,
values[2] as! Input2,
values[3] as! Input3)
}
}
@@ -5,6 +5,8 @@
// Created by Sergej Jaskiewicz on 03.12.2019.
//
#if !WASI
#if canImport(COpenCombineHelpers)
import COpenCombineHelpers
#endif
@@ -225,3 +227,5 @@ extension Publishers.Breakpoint {
var playgroundDescription: Any { return description }
}
}
#endif // !WASI
@@ -0,0 +1,268 @@
${template_header}
//
// Publishers.CombineLatest.swift.gyb
//
//
// Created by Sergej Jaskiewicz on 10.12.2019.
//
%{
from gyb_opencombine_support import (
suffix_variadic,
list_with_suffix_variadic,
indent
)
import string
instantiations = [(2, 'two', 'A second'),
(3, 'three', 'A third'),
(4, 'four', 'A fourth')]
def make_publisher_name(arity):
return suffix_variadic('CombineLatest', arity, arity - 1)
def make_upstream_types(arity, start=0):
return [str(c) for c in string.ascii_uppercase[start:(start + arity)]]
def make_upstream_generic_constraints(upstream_types, first_is_self=False):
format_string = '{0}Failure == {1}.Failure'
def format(i):
return format_string.format(upstream_types[i] + '.',
upstream_types[i + 1])
result = [format(i) for i in range(len(upstream_types) - 1)]
if first_is_self:
result.insert(0, format_string.format('', upstream_types[0]))
return result
def declare_combine_latest_method(arity, transform):
arg_count = arity - 1
declaration_format = """\
public func combineLatest<{}>(
{}
) -> {}
where {}\
"""
upstream_types = make_upstream_types(arg_count, 15)
method_generic_params = \
[upstream_type + ': Publisher' for upstream_type in upstream_types]
if transform:
method_generic_params.append('Result')
cs_method_generic_params = ', '.join(method_generic_params)
method_args = ['_ other: P'] if arg_count == 1 \
else ['_ publisher{}: {}'.format(i + 1, upstream_types[i]) \
for i in range(arg_count)]
if transform:
output_types = ['Output'] + ['{}.Output'.format(upstream_type) \
for upstream_type in upstream_types]
cs_output_types = ', '.join(output_types)
method_args \
.append('_ transform: @escaping ({}) -> Result'.format(cs_output_types))
cs_method_args = ',\n '.join(method_args)
publisher_generic_params = ['Self'] + upstream_types
cs_publisher_generic_params = ', '.join(publisher_generic_params)
publisher_name = 'Publishers.{}<{}>'.format(make_publisher_name(arity),
cs_publisher_generic_params)
if transform:
publisher_name = 'Publishers.Map<{}, Result>'.format(publisher_name)
generic_constraints = make_upstream_generic_constraints(upstream_types,
first_is_self=True)
cs_generic_constraints = ',\n '.join(generic_constraints)
declaration = declaration_format.format(cs_method_generic_params,
cs_method_args,
publisher_name,
cs_generic_constraints)
return indent(declaration, 4)
}%
// swiftlint:disable generic_type_name
// swiftlint:disable large_tuple
// MARK: - CombineLatest methods on Publisher
extension Publisher {
% for arity, _, _ in instantiations:
%
% argument_names = ['other'] \
% if arity == 2 else ['publisher{}'.format(i) for i in range(1, arity)]
% doc_cardinal = 'an additional publisher' if arity == 2 \
% else '{} additional publishers'.format(instantiations[arity - 3][1])
/// Subscribes to ${doc_cardinal} and publishes a tuple upon
/// receiving output from either publisher.
///
/// The combined publisher passes through any requests to *all* upstream publishers.
/// However, it still obeys the demand-fulfilling rule of only sending the request
/// amount downstream. If the demand isnt `.unlimited`, it drops values from upstream
/// publishers. It implements this by using a buffer size of 1 for each upstream, and
/// holds the most recent value in each buffer.
/// All upstream publishers need to finish for this publisher to finsh. If an upstream
/// publisher never publishes a value, this publisher never finishes.
/// If any of the combined publishers terminates with a failure, this publisher also
/// fails.
///
/// - Parameters:
% for i in range(arity - 1):
% param_doc = 'Another' if arity == 2 else instantiations[i][2]
/// - ${argument_names[i]}: ${param_doc} publisher to combine with this one.
% end
/// - Returns: A publisher that receives and combines elements from this and another
/// publisher.
${declare_combine_latest_method(arity, transform=False)}
{
return .init(self, ${', '.join(argument_names)})
}
/// Subscribes to ${doc_cardinal} and invokes a closure
/// upon receiving output from either publisher.
///
/// The combined publisher passes through any requests to *all* upstream publishers.
/// However, it still obeys the demand-fulfilling rule of only sending the request
/// amount downstream. If the demand isnt `.unlimited`, it drops values from upstream
/// publishers. It implements this by using a buffer size of 1 for each upstream, and
/// holds the most recent value in each buffer.
/// All upstream publishers need to finish for this publisher to finsh. If an upstream
/// publisher never publishes a value, this publisher never finishes.
/// If any of the combined publishers terminates with a failure, this publisher also
/// fails.
///
/// - Parameters:
% for i in range(arity - 1):
% param_doc = 'Another' if arity == 2 else instantiations[i][2]
/// - ${argument_names[i]}: ${param_doc} publisher to combine with this one.
% end
/// - transform: A closure that receives the most recent value from each publisher
/// and returns a new value to publish.
/// - Returns: A publisher that receives and combines elements from this and another
/// publisher.
${declare_combine_latest_method(arity, transform=True)}
{
% publisher_name = make_publisher_name(arity)
return Publishers.${publisher_name}(self, ${', '.join(argument_names)}).map {
transform(${', '.join(['${}'.format(i) for i in range(arity)])})
}
}
% end
}
// MARK: - CombineLatest publishers
extension Publishers {
% for arity, cardinal, _ in instantiations:
% publisher_name = make_publisher_name(arity)
% upstream_types = make_upstream_types(arity)
%
% upstream_generic_params = \
% [upstream_type + ': Publisher' for upstream_type in upstream_types]
%
% cs_upstream_generic_params = ', '.join(upstream_generic_params)
%
% output_types = [upstream_type + '.Output' for upstream_type in upstream_types]
%
% cs_output_types = ', '.join(output_types)
%
% upstream_generic_constraints = \
% make_upstream_generic_constraints(upstream_types)
%
% cs_upstream_generic_constraints = \
% ',\n '.join(upstream_generic_constraints)
%
% init_args = ['_ {}: {}'.format(upstream_type.lower(), upstream_type) \
% for upstream_type in upstream_types]
% cs_init_args = ',\n '.join(init_args)
%
% self_fields = [upstream_type.lower() for upstream_type in upstream_types]
/// A publisher that receives and combines the latest elements from ${cardinal}
/// publishers.
public struct ${publisher_name}<${cs_upstream_generic_params}>
: Publisher
where ${cs_upstream_generic_constraints}
{
public typealias Output = (${cs_output_types})
public typealias Failure = ${upstream_types[0]}.Failure
% for upstream_type in upstream_types:
public let ${upstream_type.lower()}: ${upstream_type}
% end
public init(
${cs_init_args}
) {
% for self_field in self_fields:
self.${self_field} = ${self_field}
% end
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Downstream.Failure == Failure,
Downstream.Input == Output
{
% cs_indented_output_types = (',\n' + (50 * ' ')).join(output_types)
typealias Inner = CombineLatest${arity}Inner<${cs_indented_output_types},
Failure,
Downstream>
let inner = Inner(downstream: subscriber, upstreamCount: ${arity})
% for i in range(arity):
${self_fields[i]}.subscribe(Inner.Side(index: ${i}, combiner: inner))
% end
subscriber.receive(subscription: inner)
}
}
% end
}
// MARK: - Equatable conformances
% for arity, _, _ in instantiations:
%
% publisher_name = make_publisher_name(arity)
%
% upstream_types = make_upstream_types(arity)
%
% constraints = [upstream_type + ': Equatable' for upstream_type in upstream_types]
% cs_constraints = ',\n'.join(constraints)
% cs_constraints = indent(cs_constraints, 8)
%
extension Publishers.${publisher_name}: Equatable
where
${cs_constraints} {}
% end
// MARK: - Inners
% for arity, _, _ in instantiations:
%
% publisher_name = make_publisher_name(arity)
%
% upstream_types = make_upstream_types(arity)
%
% input_types = ['Input{}'.format(i) for i in range(arity)]
%
% converters = ['values[{}] as! {}'.format(i, input_types[i]) for i in range(arity)]
% output_type = '({})'.format(', '.join(input_types))
private final class CombineLatest${arity}Inner<${(',\n' + (40 * ' ')).join(input_types)},
Failure,
Downstream: Subscriber>
: AbstractCombineLatest<${output_type}, Failure, Downstream>
where Downstream.Input == ${output_type},
Downstream.Failure == Failure
{
override func convert(values: [Any?]) -> (${', '.join(input_types)}) {
return (${',\n '.join(converters)})
}
}
% end
@@ -157,17 +157,17 @@ extension Publishers {
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Child.Output == Downstream.Input, Upstream.Failure == Downstream.Failure
{
let inner = Inner(downstream: subscriber,
let outer = Outer(downstream: subscriber,
maxPublishers: maxPublishers,
map: transform)
subscriber.receive(subscription: inner)
upstream.subscribe(inner)
subscriber.receive(subscription: outer)
upstream.subscribe(outer)
}
}
}
extension Publishers.FlatMap {
private final class Inner<Downstream: Subscriber>
private final class Outer<Downstream: Subscriber>
: Subscriber,
Subscription,
CustomStringConvertible,
@@ -243,7 +243,7 @@ extension Publishers.FlatMap {
subscription.request(maxPublishers)
}
fileprivate func receive(_ input: Upstream.Output) -> Subscribers.Demand {
fileprivate func receive(_ input: Input) -> Subscribers.Demand {
lock.lock()
let cancelledOrCompleted = self.cancelledOrCompleted
lock.unlock()
@@ -260,9 +260,9 @@ extension Publishers.FlatMap {
return .none
}
fileprivate func receive(completion: Subscribers.Completion<Child.Failure>) {
outerSubscription = nil
fileprivate func receive(completion: Subscribers.Completion<Failure>) {
lock.lock()
outerSubscription = nil
outerFinished = true
switch completion {
case .finished:
@@ -272,6 +272,8 @@ extension Publishers.FlatMap {
let wasAlreadyCancelledOrCompleted = cancelledOrCompleted
cancelledOrCompleted = true
for (_, subscription) in subscriptions {
// Cancelling subscriptions with the lock acquired. Not good,
// but that's what Combine does. This code path is tested.
subscription.cancel()
}
subscriptions = [:]
@@ -354,16 +356,21 @@ extension Publishers.FlatMap {
fileprivate func cancel() {
lock.lock()
if cancelledOrCompleted {
lock.unlock()
return
}
cancelledOrCompleted = true
let subscriptions = self.subscriptions
self.subscriptions = [:]
let outerSubscription = self.outerSubscription
self.outerSubscription = nil
lock.unlock()
for (_, subscription) in subscriptions {
subscription.cancel()
}
// Combine doesn't acquire the lock here. Weird.
// Combine doesn't acquire outerLock here. Weird.
outerSubscription?.cancel()
outerSubscription = nil
}
// MARK: - Reflection
@@ -471,9 +478,9 @@ extension Publishers.FlatMap {
private func releaseLockThenSendCompletionDownstreamIfNeeded(
outerFinished: Bool
) -> Bool {
#if DEBUG
#if DEBUG
lock.assertOwner() // Sanity check
#endif
#endif
if !cancelledOrCompleted && outerFinished && buffer.isEmpty &&
subscriptions.count + pendingSubscriptions == 0 {
cancelledOrCompleted = true
@@ -495,10 +502,10 @@ extension Publishers.FlatMap {
CustomReflectable,
CustomPlaygroundDisplayConvertible {
private let index: SubscriptionIndex
private let inner: Inner
private let inner: Outer
fileprivate let combineIdentifier = CombineIdentifier()
fileprivate init(index: SubscriptionIndex, inner: Inner) {
fileprivate init(index: SubscriptionIndex, inner: Outer) {
self.index = index
self.inner = inner
}
@@ -0,0 +1,197 @@
//
// Publishers.PrefixUntilOutput.swift
//
//
// Created by Sergej Jaskiewicz on 08.11.2020.
//
extension Publisher {
/// Republishes elements until another publisher emits an element.
///
/// After the second publisher publishes an element, the publisher returned by this
/// method finishes.
///
/// - Parameter publisher: A second publisher.
/// - Returns: A publisher that republishes elements until the second publisher
/// publishes an element.
public func prefix<Other: Publisher>(
untilOutputFrom publisher: Other
) -> Publishers.PrefixUntilOutput<Self, Other> {
return .init(upstream: self, other: publisher)
}
}
extension Publishers {
public struct PrefixUntilOutput<Upstream: Publisher, Other: Publisher>: Publisher {
public typealias Output = Upstream.Output
public typealias Failure = Upstream.Failure
/// The publisher from which this publisher receives elements.
public let upstream: Upstream
/// Another publisher, whose first output causes this publisher to finish.
public let other: Other
public init(upstream: Upstream, other: Other) {
self.upstream = upstream
self.other = other
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Downstream.Failure == Failure, Downstream.Input == Output
{
upstream.subscribe(Inner(downstream: subscriber, trigger: other))
}
}
}
extension Publishers.PrefixUntilOutput {
private final class Inner<Downstream: Subscriber>
: Subscriber,
Subscription
where Downstream.Input == Upstream.Output, Downstream.Failure == Upstream.Failure
{
typealias Input = Upstream.Output
typealias Failure = Upstream.Failure
private struct Termination: Subscriber {
let inner: Inner
var combineIdentifier: CombineIdentifier {
return inner.combineIdentifier
}
func receive(subscription: Subscription) {
inner.terminationReceive(subscription: subscription)
}
func receive(_ input: Other.Output) -> Subscribers.Demand {
return inner.terminationReceive(input)
}
func receive(completion: Subscribers.Completion<Other.Failure>) {
inner.terminationReceive(completion: completion)
}
}
private var termination: Termination?
private var prefixState = SubscriptionStatus.awaitingSubscription
private var terminationState = SubscriptionStatus.awaitingSubscription
private var triggered = false
private let lock = UnfairLock.allocate()
private let downstream: Downstream
init(downstream: Downstream, trigger: Other) {
self.downstream = downstream
let termination = Termination(inner: self)
self.termination = termination
trigger.subscribe(termination)
}
deinit {
lock.deallocate()
}
func receive(subscription: Subscription) {
lock.lock()
guard case .awaitingSubscription = prefixState else {
lock.unlock()
subscription.cancel()
return
}
prefixState = triggered ? .terminal : .subscribed(subscription)
lock.unlock()
downstream.receive(subscription: self)
}
func receive(_ input: Input) -> Subscribers.Demand {
lock.lock()
guard case .subscribed = prefixState else {
lock.unlock()
return .none
}
lock.unlock()
return downstream.receive(input)
}
func receive(completion: Subscribers.Completion<Failure>) {
lock.lock()
let prefixState = self.prefixState
let terminationSubscription = terminationState.subscription
self.prefixState = .terminal
terminationState = .terminal
termination = nil
lock.unlock()
terminationSubscription?.cancel()
if case .subscribed = prefixState {
downstream.receive(completion: completion)
}
}
func request(_ demand: Subscribers.Demand) {
lock.lock()
guard case let .subscribed(subscription) = prefixState else {
lock.unlock()
return
}
lock.unlock()
subscription.request(demand)
}
func cancel() {
lock.lock()
let prefixSubscription = prefixState.subscription
let terminationSubscription = terminationState.subscription
prefixState = .terminal
terminationState = .terminal
lock.unlock()
prefixSubscription?.cancel()
terminationSubscription?.cancel()
}
// MARK: - Private
private func terminationReceive(subscription: Subscription) {
lock.lock()
guard case .awaitingSubscription = terminationState else {
lock.unlock()
subscription.cancel()
return
}
terminationState = .subscribed(subscription)
lock.unlock()
subscription.request(.max(1))
}
private func terminationReceive(_ input: Other.Output) -> Subscribers.Demand {
lock.lock()
guard case .subscribed = terminationState else {
lock.unlock()
return .none
}
let prefixSubscription = prefixState.subscription
prefixState = .terminal
terminationState = .terminal
termination = nil
triggered = true
lock.unlock()
prefixSubscription?.cancel()
downstream.receive(completion: .finished)
return .none
}
private func terminationReceive(
completion: Subscribers.Completion<Other.Failure>
) {
lock.lock()
terminationState = .terminal
termination = nil
lock.unlock()
}
}
}
@@ -0,0 +1,274 @@
//
// Publishers.Retry.swift
//
//
// Created by Sergej Jaskiewicz on 28.06.2020.
//
extension Publisher {
/// Attempts to recreate a failed subscription with the upstream publisher up to
/// the number of times you specify.
///
/// Use `retry(_:)` to try connecting to an upstream publisher after a failed
/// connection attempt.
///
/// In the example below, a `URLSession.DataTaskPublisher` attempts to connect to
/// a remote URL. If the connection attempt succeeds, it publishes the remote
/// services HTML to the downstream publisher and completes normally. Otherwise,
/// the retry operator attempts to reestablish the connection. If after three attempts
/// the publisher still cant connect to the remote URL, the `catch(_:)` operator
/// replaces the error with a new publisher that publishes a connection timed out
/// HTML page. After the downstream subscriber receives the timed out message,
/// the stream completes normally.
///
/// struct WebSiteData: Codable {
/// var rawHTML: String
/// }
///
/// let myURL = URL(string: "https://www.example.com")
///
/// cancellable = URLSession.shared.dataTaskPublisher(for: myURL!)
/// .retry(3)
/// .map { page -> WebSiteData in
/// WebSiteData(rawHTML: String(decoding: page.data, as: UTF8.self))
/// }
/// .catch { error in
/// Just(
/// WebSiteData(
/// rawHTML: "<HTML>Unable to load page - timed out.</HTML>"
/// )
/// )
/// }
/// .sink(receiveCompletion: { print ("completion: \($0)") },
/// receiveValue: { print ("value: \($0)") })
///
/// // Prints: The HTML content from the remote URL upon a successful connection,
/// // or returns "<HTML>Unable to load page - timed out.</HTML>" if
/// // the number of retries exceeds the specified value.
///
/// After exceeding the specified number of retries, the publisher passes the failure
/// to the downstream receiver.
/// - Parameter retries: The number of times to attempt to recreate the subscription.
/// - Returns: A publisher that attempts to recreate its subscription to a failed
/// upstream publisher.
public func retry(_ retries: Int) -> Publishers.Retry<Self> {
return .init(upstream: self, retries: retries)
}
}
extension Publishers {
/// A publisher that attempts to recreate its subscription to a failed upstream
/// publisher.
public struct Retry<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 retry attempts to perform.
///
/// If `nil`, this publisher attempts to reconnect with the upstream publisher
/// an unlimited number of times.
public let retries: Int?
/// Creates a publisher that attempts to recreate its subscription to a failed
/// upstream publisher.
///
/// - Parameters:
/// - upstream: The publisher from which this publisher receives its elements.
/// - retries: The maximum number of retry attempts to perform. If `nil`, this
/// publisher attempts to reconnect with the upstream publisher an unlimited
/// number of times.
public init(upstream: Upstream, retries: Int?) {
self.upstream = upstream
self.retries = retries
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Downstream.Input == Output, Downstream.Failure == Failure
{
upstream.subscribe(Inner(parent: self, downstream: subscriber))
}
}
}
extension Publishers.Retry: Equatable where Upstream: Equatable {}
extension Publishers.Retry {
private final class Inner<Downstream: Subscriber>
: Subscriber,
Subscription,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
where Downstream.Failure == Failure, Downstream.Input == Output
{
typealias Input = Upstream.Output
typealias Failure = Upstream.Failure
private enum State {
case ready(Publishers.Retry<Upstream>, Downstream)
case terminal
}
private enum Chances {
case finite(Int)
case infinite
}
private let lock = UnfairLock.allocate()
private var state: State
private var upstreamSubscription: Subscription?
private var remaining: Chances
private var downstreamNeedsSubscription = true
private var downstreamDemand = Subscribers.Demand.none
private var completionRecursion = false
private var needsSubscribe = false
init(parent: Publishers.Retry<Upstream>, downstream: Downstream) {
state = .ready(parent, downstream)
remaining = parent.retries.map(Chances.finite) ?? .infinite
}
deinit {
lock.deallocate()
}
func receive(subscription: Subscription) {
lock.lock()
guard case let .ready(_, downstream) = state, upstreamSubscription == nil
else {
lock.unlock()
subscription.cancel()
return
}
upstreamSubscription = subscription
let downstreamDemand = self.downstreamDemand
let downstreamNeedsSubscription = self.downstreamNeedsSubscription
self.downstreamNeedsSubscription = false
lock.unlock()
if downstreamNeedsSubscription {
downstream.receive(subscription: self)
}
if downstreamDemand != .none {
subscription.request(downstreamDemand)
}
}
func receive(_ input: Input) -> Subscribers.Demand {
lock.lock()
guard case let .ready(_, downstream) = state else {
lock.unlock()
return .none
}
downstreamDemand -= 1
lock.unlock()
let newDemand = downstream.receive(input)
if newDemand == .none { return .none }
lock.lock()
downstreamDemand += newDemand
if let upstreamSubscription = self.upstreamSubscription {
lock.unlock()
upstreamSubscription.request(newDemand)
} else {
lock.unlock()
}
return .none
}
func receive(completion: Subscribers.Completion<Failure>) {
lock.lock()
guard case let .ready(parent, downstream) = state else {
lock.unlock()
return
}
if case .failure = completion {
upstreamSubscription = nil
switch remaining {
case .finite(0):
break
case .finite(let attempts):
remaining = .finite(attempts - 1)
fallthrough
case .infinite:
if completionRecursion {
needsSubscribe = true
lock.unlock()
return
}
repeat {
completionRecursion = true
needsSubscribe = false
lock.unlock()
parent.upstream.subscribe(self)
lock.lock()
completionRecursion = false
} while needsSubscribe
lock.unlock()
return
}
}
state = .terminal
lock.unlock()
downstream.receive(completion: completion)
}
func request(_ demand: Subscribers.Demand) {
lock.lock()
guard case .ready = state else {
lock.unlock()
return
}
downstreamDemand += demand
if let upstreamSubscription = self.upstreamSubscription {
lock.unlock()
upstreamSubscription.request(demand)
} else {
lock.unlock()
}
}
func cancel() {
lock.lock()
guard case .ready = state else {
lock.unlock()
return
}
state = .terminal
if let upstreamSubscription = self.upstreamSubscription {
lock.unlock()
upstreamSubscription.cancel()
} else {
lock.unlock()
}
}
var description: String { return "Retry" }
var customMirror: Mirror {
return Mirror(self, children: EmptyCollection())
}
var playgroundDescription: Any { return description }
}
}
@@ -0,0 +1,362 @@
//
// Publishers.Throttle.swift
//
//
// Created by Stuart Austin on 14/11/2020.
//
extension Publisher {
// swiftlint:disable generic_type_name line_length
/// Publishes either the most-recent or first element published by the upstream
/// publisher in the specified time interval.
///
/// Use `throttle(for:scheduler:latest:`` to selectively republish elements from
/// an upstream publisher during an interval you specify. Other elements received from
/// the upstream in the throttling interval arent republished.
///
/// In the example below, a `Timer.TimerPublisher` produces elements on 3-second
/// intervals; the `throttle(for:scheduler:latest:)` operator delivers the first
/// event, then republishes only the latest event in the following ten second
/// intervals:
///
/// cancellable = Timer.publish(every: 3.0, on: .main, in: .default)
/// .autoconnect()
/// .print("\(Date().description)")
/// .throttle(for: 10.0, scheduler: RunLoop.main, latest: true)
/// .sink(
/// receiveCompletion: { print ("Completion: \($0).") },
/// receiveValue: { print("Received Timestamp \($0).") }
/// )
///
/// // Prints:
/// // Publish at: 2020-03-19 18:26:54 +0000: receive value: (2020-03-19 18:26:57 +0000)
/// // Received Timestamp 2020-03-19 18:26:57 +0000.
/// // Publish at: 2020-03-19 18:26:54 +0000: receive value: (2020-03-19 18:27:00 +0000)
/// // Publish at: 2020-03-19 18:26:54 +0000: receive value: (2020-03-19 18:27:03 +0000)
/// // Publish at: 2020-03-19 18:26:54 +0000: receive value: (2020-03-19 18:27:06 +0000)
/// // Publish at: 2020-03-19 18:26:54 +0000: receive value: (2020-03-19 18:27:09 +0000)
/// // Received Timestamp 2020-03-19 18:27:09 +0000.
///
/// - Parameters:
/// - interval: The interval at which to find and emit either the most recent or
/// the first element, expressed in the time system of the scheduler.
/// - scheduler: The scheduler on which to publish elements.
/// - latest: A Boolean value that indicates whether to publish the most recent
/// element. If `false`, the publisher emits the first element received during
/// the interval.
/// - Returns: A publisher that emits either the most-recent or first element received
/// during the specified interval.
public func throttle<S>(for interval: S.SchedulerTimeType.Stride,
scheduler: S,
latest: Bool) -> Publishers.Throttle<Self, S>
where S: Scheduler
{
return .init(upstream: self,
interval: interval,
scheduler: scheduler,
latest: latest)
}
// swiftlint:enable generic_type_name line_length
}
extension Publishers {
/// A publisher that publishes either the most-recent or first element published by
/// the upstream publisher in a specified time interval.
public struct Throttle<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 interval in which to find and emit the most recent element.
public let interval: Context.SchedulerTimeType.Stride
/// The scheduler on which to publish elements.
public let scheduler: Context
/// A Boolean value indicating whether to publish the most recent element.
///
/// If `false`, the publisher emits the first element received during
/// the interval.
public let latest: Bool
public init(upstream: Upstream,
interval: Context.SchedulerTimeType.Stride,
scheduler: Context,
latest: Bool) {
self.upstream = upstream
self.interval = interval
self.scheduler = scheduler
self.latest = latest
}
// swiftlint:disable generic_type_name
/// Attaches the specified subscriber to this publisher.
///
/// Implementations of ``Publisher`` must implement this method.
///
/// The provided implementation of ``Publisher/subscribe(_:)-4u8kn``calls
/// this method.
///
/// - Parameter subscriber: The subscriber to attach to this ``Publisher``,
/// after which it can receive values.
public func receive<S>(subscriber: S)
where S: Subscriber, Upstream.Failure == S.Failure, Upstream.Output == S.Input
{
let inner = Inner(interval: interval,
scheduler: scheduler,
latest: latest,
downstream: subscriber)
upstream.subscribe(inner)
}
// swiftlint:enable generic_type_name
}
}
extension Publishers.Throttle {
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 awaitingSubscription(Downstream)
case subscribed(Subscription, Downstream)
case pendingTerminal(Subscription, Downstream)
case terminal
}
private let lock = UnfairLock.allocate()
private let interval: Context.SchedulerTimeType.Stride
private let scheduler: Context
private let latest: Bool
private var state: State
private let downstreamLock = UnfairRecursiveLock.allocate()
private var lastEmissionTime: Context.SchedulerTimeType?
private var pendingInput: Input?
private var pendingCompletion: Subscribers.Completion<Failure>?
private var demand: Subscribers.Demand = .none
private var lastTime: Context.SchedulerTimeType
init(interval: Context.SchedulerTimeType.Stride,
scheduler: Context,
latest: Bool,
downstream: Downstream) {
self.state = .awaitingSubscription(downstream)
self.interval = interval
self.scheduler = scheduler
self.latest = latest
self.lastTime = scheduler.now
}
deinit {
lock.deallocate()
downstreamLock.deallocate()
}
func receive(subscription: Subscription) {
lock.lock()
guard case let .awaitingSubscription(downstream) = state else {
lock.unlock()
subscription.cancel()
return
}
self.lastTime = scheduler.now
state = .subscribed(subscription, downstream)
lock.unlock()
subscription.request(.unlimited)
downstreamLock.lock()
downstream.receive(subscription: self)
downstreamLock.unlock()
}
func receive(_ input: Input) -> Subscribers.Demand {
lock.lock()
guard case .subscribed = state else {
lock.unlock()
return .none
}
let lastTime = scheduler.now
self.lastTime = lastTime
guard demand > .none else {
lock.unlock()
return .none
}
let hasScheduledOutput = (pendingInput != nil || pendingCompletion != nil)
if hasScheduledOutput && latest {
pendingInput = input
lock.unlock()
} else if !hasScheduledOutput {
let minimumEmissionTime =
lastEmissionTime.map { $0.advanced(by: interval) }
let emissionTime =
minimumEmissionTime.map { Swift.max(lastTime, $0) } ?? lastTime
demand -= 1
pendingInput = input
lock.unlock()
let action: () -> Void = { [weak self] in
self?.scheduledEmission()
}
if emissionTime == lastTime {
scheduler.schedule(action)
} else {
scheduler.schedule(after: emissionTime, action)
}
} else {
lock.unlock()
}
return .none
}
func receive(completion: Subscribers.Completion<Failure>) {
lock.lock()
guard case let .subscribed(subscription, downstream) = state else {
lock.unlock()
return
}
let lastTime = scheduler.now
self.lastTime = lastTime
state = .pendingTerminal(subscription, downstream)
let hasScheduledOutput = (pendingInput != nil || pendingCompletion != nil)
if hasScheduledOutput && pendingCompletion == nil {
pendingCompletion = completion
lock.unlock()
} else if !hasScheduledOutput {
pendingCompletion = completion
lock.unlock()
scheduler.schedule { [weak self] in
self?.scheduledEmission()
}
} else {
lock.unlock()
}
}
private func scheduledEmission() {
lock.lock()
let downstream: Downstream
switch state {
case .awaitingSubscription, .terminal:
lock.unlock()
return
case let .subscribed(_, foundDownstream),
let .pendingTerminal(_, foundDownstream):
downstream = foundDownstream
}
if self.pendingInput != nil && self.pendingCompletion == nil {
lastEmissionTime = scheduler.now
}
let pendingInput = self.pendingInput
let pendingCompletion = self.pendingCompletion
self.pendingInput = nil
self.pendingCompletion = nil
if pendingCompletion != nil {
state = .terminal
}
lock.unlock()
downstreamLock.lock()
let newDemand: Subscribers.Demand
if let input = pendingInput {
newDemand = downstream.receive(input)
} else {
newDemand = .none
}
if let completion = pendingCompletion {
downstream.receive(completion: completion)
}
downstreamLock.unlock()
guard newDemand > 0 else { return }
self.lock.lock()
demand += newDemand
self.lock.unlock()
}
func request(_ demand: Subscribers.Demand) {
guard demand > 0 else { return }
lock.lock()
guard case .subscribed = state else {
lock.unlock()
return
}
self.demand += demand
lock.unlock()
}
func cancel() {
lock.lock()
let subscription: Subscription?
switch state {
case let .subscribed(existingSubscription, _),
let .pendingTerminal(existingSubscription, _):
subscription = existingSubscription
case .awaitingSubscription, .terminal:
subscription = nil
}
state = .terminal
lock.unlock()
subscription?.cancel()
}
var description: String { return "Throttle" }
var customMirror: Mirror { return Mirror(self, children: EmptyCollection()) }
var playgroundDescription: Any { return description }
}
}
@@ -200,7 +200,7 @@ extension DispatchQueue {
/// - Parameter exactly: A binary integer representing a time interval.
public init?<Source: BinaryInteger>(exactly source: Source) {
guard let value = Int(exactly: source) else { return nil }
self = .nanoseconds(value)
self = .seconds(value)
}
public static func < (lhs: Stride, rhs: Stride) -> Bool {
@@ -60,7 +60,12 @@ internal struct Timer {
repeats: Bool,
block: @escaping (Timer) -> Void
) {
self.init(fire: Date(), interval: timeInterval, repeats: repeats, block: block)
self.init(
fire: Date() + timeInterval,
interval: timeInterval,
repeats: repeats,
block: block
)
}
internal var tolerance: TimeInterval {
@@ -0,0 +1,11 @@
#if canImport(Combine)
@_exported import Combine
#else
@_exported import OpenCombine
#if canImport(OpenCombineDispatch)
@_exported import OpenCombineDispatch
#endif
#if canImport(OpenCombineFoundation)
@_exported import OpenCombineFoundation
#endif
#endif
+1 -1
View File
@@ -5,4 +5,4 @@ disabled_rules:
- explicit_acl
- explicit_top_level_acl
- explicit_enum_raw_value
- untyped_error_in_catch
@@ -5,6 +5,8 @@
// Created by Sergej Jaskiewicz on 26.08.2019.
//
#if !WASI
import Dispatch
import XCTest
@@ -254,7 +256,7 @@ final class DispatchQueueSchedulerTests: XCTestCase {
XCTAssertEqual((2 as Stride).magnitude, 2_000_000_000)
XCTAssertNil(Stride(exactly: UInt64.max))
XCTAssertEqual(Stride(exactly: 871 as UInt64)?.magnitude, 871)
XCTAssertEqual(Stride(exactly: 2 as UInt64)?.magnitude, 2_000_000_000)
}
func testStrideFromTooMuchSecondsCrashes() {
@@ -570,7 +572,7 @@ private typealias Scheduler = DispatchQueue.OCombine
private let mainScheduler = DispatchQueue.main.ocombine
private let backgroundScheduler = DispatchQueue.global(qos: .background).ocombine
#endif
#endif // OPENCOMBINE_COMPATIBILITY_TEST || !canImport(Combine)
@available(macOS 10.15, iOS 13.0, *)
private typealias Stride = Scheduler.SchedulerTimeType.Stride
@@ -578,3 +580,5 @@ private typealias Stride = Scheduler.SchedulerTimeType.Stride
private struct KeyedWrapper<Value: Codable & Equatable>: Codable, Equatable {
let value: Value
}
#endif // !WASI
@@ -5,6 +5,8 @@
// Created by Sergej Jaskiewicz on 10.12.2019.
//
#if !WASI
import Foundation
import XCTest
@@ -61,3 +63,5 @@ final class JSONDecoderTests: XCTestCase {
cancellable.cancel()
}
}
#endif // !WASI
@@ -5,6 +5,8 @@
// Created by Sergej Jaskiewicz on 10.12.2019.
//
#if !WASI
import Foundation
import XCTest
@@ -65,3 +67,5 @@ final class JSONEncoderTests: XCTestCase {
cancellable.cancel()
}
}
#endif // !WASI
@@ -5,6 +5,8 @@
// Created by Sergej Jaskiewicz on 10.12.2019.
//
#if !WASI
import Foundation
import XCTest
@@ -630,4 +632,6 @@ extension Notification.Name {
self.init(rawValue: rawValue)
}
}
#endif
#endif // !canImport(Darwin) && swift(<5.1)
#endif // !WASI
@@ -5,6 +5,10 @@
// Created by Sergej Jaskiewicz on 14.06.2020.
//
// OperationQueue has serious bugs in swift-corelibs-foundation prior to Swift 5.3.
// (see https://github.com/apple/swift-corelibs-foundation/pull/2779)
#if canImport(Darwin) || swift(>=5.3) && !WASI // TEST_DISCOVERY_CONDITION
import Foundation
import XCTest
@@ -15,10 +19,6 @@ import OpenCombine
import OpenCombineFoundation
#endif
// OperationQueue has serious bugs in swift-corelibs-foundation prior to Swift 5.3.
// (see https://github.com/apple/swift-corelibs-foundation/pull/2779)
#if canImport(Darwin) || swift(>=5.3) // TEST_DISCOVERY_CONDITION
@available(macOS 10.15, iOS 13.0, *)
final class OperationQueueSchedulerTests: XCTestCase {
@@ -225,7 +225,7 @@ final class OperationQueueSchedulerTests: XCTestCase {
}
func testScheduleRepeatingWithRealQueue() {
let mainQueue = OperationQueue.main
let mainQueueScheduler = makeScheduler(OperationQueue.main)
let expectation10ticks = expectation(description: "10 ticks")
expectation10ticks.expectedFulfillmentCount = 10
@@ -234,13 +234,12 @@ final class OperationQueueSchedulerTests: XCTestCase {
let ticks = Atomic([TimeInterval]())
let desiredDelay: TimeInterval = 0.7
let desiredInterval: TimeInterval = 0.3
let desiredDelay: TimeInterval = 0.8
let desiredInterval: TimeInterval = 0.5
let cancellable = executeOnBackgroundThread { () -> Cancellable in
let scheduler = makeScheduler(mainQueue)
return scheduler
.schedule(after: scheduler.now.advanced(by: .init(desiredDelay)),
let cancellable = executeOnBackgroundThread {
mainQueueScheduler
.schedule(after: mainQueueScheduler.now.advanced(by: .init(desiredDelay)),
interval: .init(desiredInterval)) {
XCTAssertTrue(Thread.isMainThread)
ticks.append(Date().timeIntervalSinceReferenceDate)
@@ -256,7 +255,7 @@ final class OperationQueueSchedulerTests: XCTestCase {
RunLoop.main.run(until: Date() + 0.001)
XCTAssertEqual(ticks.count, 0)
wait(for: [expectation10ticks], timeout: 5)
wait(for: [expectation10ticks], timeout: 10)
if ticks.isEmpty {
XCTFail("The scheduler doesn't work")
@@ -471,4 +470,4 @@ private final class TestOperationQueue: OperationQueue {
#endif // canImport(Darwin)
}
#endif // canImport(Darwin) || swift(>=5.3)
#endif // canImport(Darwin) || swift(>=5.3) && !WASI
@@ -5,6 +5,10 @@
// Created by Sergej Jaskiewicz on 10.12.2019.
//
// PropertyListEncoder and PropertyListDecoder are unavailable in
// swift-corelibs-foundation prior to Swift 5.1.
#if canImport(Darwin) || swift(>=5.1) && !WASI // TEST_DISCOVERY_CONDITION
import Foundation
import XCTest
@@ -15,10 +19,6 @@ import OpenCombine
import OpenCombineFoundation
#endif
// PropertyListEncoder and PropertyListDecoder are unavailable in
// swift-corelibs-foundation prior to Swift 5.1.
#if canImport(Darwin) || swift(>=5.1) // TEST_DISCOVERY_CONDITION
@available(macOS 10.15, iOS 13.0, *)
final class PropertyListDecoderTests: XCTestCase {
func testSuccessfullyDecode() {
@@ -79,4 +79,4 @@ final class PropertyListDecoderTests: XCTestCase {
}
}
#endif // canImport(Darwin) || swift(>=5.1)
#endif // canImport(Darwin) || swift(>=5.1) && !WASI
@@ -5,6 +5,10 @@
// Created by Sergej Jaskiewicz on 10.12.2019.
//
// PropertyListEncoder and PropertyListDecoder are unavailable in
// swift-corelibs-foundation prior to Swift 5.1.
#if canImport(Darwin) || swift(>=5.1) && !WASI // TEST_DISCOVERY_CONDITION
import Foundation
import XCTest
@@ -15,10 +19,6 @@ import OpenCombine
import OpenCombineFoundation
#endif
// PropertyListEncoder and PropertyListDecoder are unavailable in
// swift-corelibs-foundation prior to Swift 5.1.
#if canImport(Darwin) || swift(>=5.1) // TEST_DISCOVERY_CONDITION
@available(macOS 10.15, iOS 13.0, *)
final class PropertyListEncoderTests: XCTestCase {
@@ -84,4 +84,4 @@ final class PropertyListEncoderTests: XCTestCase {
}
}
#endif // canImport(Darwin) || swift(>=5.1)
#endif // canImport(Darwin) || swift(>=5.1) && !WASI
@@ -5,6 +5,8 @@
// Created by Sergej Jaskiewicz on 14.12.2019.
//
#if !WASI
import Foundation
import XCTest
@@ -627,3 +629,5 @@ extension RunLoopScheduler.SchedulerTimeType.Stride: TimeIntervalBackedScheduler
extension RunLoopScheduler.SchedulerTimeType: DateBackedSchedulerTimeType {}
extension RunLoopScheduler: RunLoopLikeScheduler {}
#endif // !WASI
@@ -5,6 +5,8 @@
// Created by Sergej Jaskiewicz on 23.06.2020.
//
#if !WASI
import Foundation
import XCTest
@@ -214,3 +216,5 @@ private typealias TimerPublisher = Timer.TimerPublisher
#else
private typealias TimerPublisher = Timer.OCombine.TimerPublisher
#endif
#endif // !WASI
@@ -7,6 +7,8 @@
// swiftlint:disable multiline_arguments
#if !WASI
import Foundation
import XCTest
@@ -51,13 +53,14 @@ final class URLSessionTests: XCTestCase {
private let unknownError = URLError(.unknown)
func testDataTaskPublisherFromURL() {
let publisher = makePublisher(TestURLSession(testDataTask: .init()), testURL)
let publisher = makePublisher(TestURLSession.withTestDataTask(.create()), testURL)
let expectedRequest = URLRequest(url: testURL)
XCTAssertEqual(publisher.request, expectedRequest)
}
func testDataTaskPublisherFromRequest() {
let publisher = makePublisher(TestURLSession(testDataTask: .init()), testRequest)
let publisher = makePublisher(TestURLSession.withTestDataTask(.create()),
testRequest)
XCTAssertEqual(publisher.request, testRequest)
}
@@ -124,8 +127,8 @@ final class URLSessionTests: XCTestCase {
}
func testRequesting() throws {
let dataTask = TestURLSessionDataTask()
let session = TestURLSession(testDataTask: dataTask)
let dataTask = TestURLSessionDataTask.create()
let session = TestURLSession.withTestDataTask(dataTask)
let publisher = makePublisher(session, testRequest)
var downstreamSubscription: Subscription?
let tracking = TrackingSubscriber(
@@ -164,8 +167,8 @@ final class URLSessionTests: XCTestCase {
}
func testCancelAlreadyCancelled() throws {
let dataTask = TestURLSessionDataTask()
let session = TestURLSession(testDataTask: dataTask)
let dataTask = TestURLSessionDataTask.create()
let session = TestURLSession.withTestDataTask(dataTask)
let publisher = makePublisher(session, testRequest)
var downstreamSubscription: Subscription?
let tracking = TrackingSubscriber(
@@ -190,8 +193,8 @@ final class URLSessionTests: XCTestCase {
}
func testCrashesOnZeroDemand() throws {
let dataTask = TestURLSessionDataTask()
let session = TestURLSession(testDataTask: dataTask)
let dataTask = TestURLSessionDataTask.create()
let session = TestURLSession.withTestDataTask(dataTask)
let publisher = makePublisher(session, testURL)
var downstreamSubscription: Subscription?
let tracking = TrackingSubscriber(
@@ -205,8 +208,8 @@ final class URLSessionTests: XCTestCase {
}
func testURLSessionSubscriptionReflection() throws {
let dataTask = TestURLSessionDataTask()
let session = TestURLSession(testDataTask: dataTask)
let dataTask = TestURLSessionDataTask.create()
let session = TestURLSession.withTestDataTask(dataTask)
let publisher = makePublisher(session, testURL)
try testSubscriptionReflection(
description: "DataTaskPublisher",
@@ -227,8 +230,8 @@ final class URLSessionTests: XCTestCase {
_ response: URLResponse?,
_ error: Error?,
expected: [TrackingSubscriber.Event]) {
let dataTask = TestURLSessionDataTask()
let session = TestURLSession(testDataTask: dataTask)
let dataTask = TestURLSessionDataTask.create()
let session = TestURLSession.withTestDataTask(dataTask)
let publisher = makePublisher(session, testRequest)
let tracking = TrackingSubscriber(receiveSubscription: { $0.request(.max(1)) })
publisher.subscribe(tracking)
@@ -290,16 +293,25 @@ private class TestURLSession: URLSession {
private(set) var history = [Event]()
private(set) var dataTaskCompletionHandlers: [(Data?, URLResponse?, Error?) -> Void]
private(set) var dataTaskCompletionHandlers =
[(Data?, URLResponse?, Error?) -> Void]()
private let testDataTask: TestURLSessionDataTask
private var testDataTask: TestURLSessionDataTask?
init(testDataTask: TestURLSessionDataTask) {
self.testDataTask = testDataTask
self.dataTaskCompletionHandlers = []
#if !canImport(Darwin)
super.init(configuration: .default)
static func withTestDataTask(
_ testDataTask: TestURLSessionDataTask
) -> TestURLSession {
// This dance is to avoid the deprecation warning for the URLSession
// default initializer. Believe me, I've tried to make it less ugly with
// no success.
#if canImport(Darwin)
let sessionClass = TestURLSession.self as NSObject.Type
let session = sessionClass.init() as! TestURLSession
#else
let session = TestURLSession(configuration: .default)
#endif
session.testDataTask = testDataTask
return session
}
// MARK: Testing
@@ -377,12 +389,12 @@ private class TestURLSession: URLSession {
override func dataTask(with request: URLRequest) -> URLSessionDataTask {
history.append(.dataTaskWithRequest(request))
return testDataTask
return testDataTask!
}
override func dataTask(with url: URL) -> URLSessionDataTask {
history.append(.dataTaskWithURL(url))
return testDataTask
return testDataTask!
}
override func dataTask(
@@ -391,7 +403,7 @@ private class TestURLSession: URLSession {
) -> URLSessionDataTask {
history.append(.dataTaskWithURLAndCompletion(url))
dataTaskCompletionHandlers.append(completionHandler)
return testDataTask
return testDataTask!
}
override func dataTask(
@@ -400,7 +412,7 @@ private class TestURLSession: URLSession {
) -> URLSessionDataTask {
history.append(.dataTaskWithRequestAndCompletion(request))
dataTaskCompletionHandlers.append(completionHandler)
return testDataTask
return testDataTask!
}
override func uploadTask(with request: URLRequest,
@@ -554,7 +566,18 @@ private final class TestURLSessionDataTask: URLSessionDataTask {
private(set) var history = [Event]()
override init() {}
static func create() -> TestURLSessionDataTask {
// This dance is to avoid the deprecation warning for the URLSessionDataTask
// default initializer. Believe me, I've tried to make it less ugly with
// no success.
#if canImport(Darwin)
let dataTaskClass = TestURLSessionDataTask.self as NSObject.Type
let dataTask = dataTaskClass.init() as! TestURLSessionDataTask
#else
let dataTask = TestURLSessionDataTask()
#endif
return dataTask
}
override var taskIdentifier: Int {
history.append(.taskIdentifier)
@@ -721,6 +744,8 @@ private func makePublisher(
) -> URLSession.OCombine.DataTaskPublisher {
return session.ocombine.dataTaskPublisher(for: request)
}
#endif
#endif // OPENCOMBINE_COMPATIBILITY_TEST || !canImport(Combine)
#endif // canImport(Darwin)
#endif // !WASI
@@ -29,7 +29,7 @@ extension XCTest {
// Taken from swift-corelibs-foundation and slightly modified for OpenCombine
@available(macOS 10.13, iOS 8.0, *)
func assertCrashes(within block: () throws -> Void) rethrows {
#if !Xcode && !os(iOS) && !os(watchOS) && !os(tvOS)
#if !Xcode && !os(iOS) && !os(watchOS) && !os(tvOS) && !WASI
let childProcessEnvVariable = "OPENCOMBINE_TEST_PERFORM_ASSERT_CRASHES_BLOCKS"
let childProcessEnvVariableOnValue = "YES"
@@ -82,6 +82,6 @@ extension XCTest {
printDiagnostics()
}
}
#endif
#endif // !Xcode && !os(iOS) && !os(watchOS) && !os(tvOS) && !WASI
}
}
@@ -98,9 +98,14 @@ extension XCTest {
}
}
enum CancelBeforeSubscriptionBehavior {
case crash
case history([CustomSubscription.Event])
}
func testCancelBeforeSubscription<Value, Operator: Publisher>(
inputType: Value.Type,
shouldCrash: Bool,
expected: CancelBeforeSubscriptionBehavior,
_ makeOperator: (CustomConnectablePublisherBase<Value, Never>) -> Operator
) {
@@ -109,17 +114,23 @@ extension XCTest {
let tracking = TrackingSubscriberBase<Operator.Output, Operator.Failure>()
operatorPublisher.subscribe(tracking)
guard let subscription = publisher.erasedSubscriber as? Subscription else {
guard let downstreamSubscription = publisher.erasedSubscriber as? Subscription
else {
XCTFail("The subscriber must also be a subscription")
return
}
if shouldCrash {
switch expected {
case .crash:
assertCrashes {
subscription.cancel()
downstreamSubscription.cancel()
}
} else {
subscription.cancel()
case let .history(history):
downstreamSubscription.cancel()
let subscription = CustomSubscription()
publisher.send(subscription: subscription)
XCTAssertEqual(subscription.history, history)
}
}
@@ -203,4 +214,19 @@ func unreachable() -> Never {
fatalError("unreachable")
}
func fromNever<T>(_ resultType: T.Type) -> (Never) -> T {
// This is to avoid the 'Will never be executed' warning.
//
// The first variant doesn't produce warnings in Swift 5.1, but doesn't compile
// with early Swift versions.
//
// The second variant compiles with all Swift versions,
// but produces a warning in Swift 5.1.
#if swift(>=5.1)
return { (_: Never) -> T in }
#else
return { switch $0 {} }
#endif
}
// swiftlint:enable generic_type_name
@@ -5,6 +5,8 @@
// Created by Sergej Jaskiewicz on 04.02.2020.
//
#if !WASI
#if canImport(Darwin)
import Darwin
#elseif canImport(Glibc)
@@ -81,3 +83,5 @@ func executeOnBackgroundThread<ResultType>(
}
}
}
#endif // !WASI
@@ -5,20 +5,34 @@
// Created by Sergej Jaskiewicz on 11.06.2019.
//
#if !WASI
import Dispatch
#endif
import Foundation
import XCTest
func race(times: Int = 100, _ bodies: () -> Void...) {
#if WASI
for body in bodies {
for _ in 0..<times {
body()
}
}
#else
DispatchQueue.concurrentPerform(iterations: bodies.count) {
for _ in 0..<times {
bodies[$0]()
}
}
#endif
}
final class Atomic<Value> {
#if !WASI
let lock = NSLock()
#endif
private var _value: Value
init(_ initialValue: Value) {
@@ -26,20 +40,29 @@ final class Atomic<Value> {
}
var value: Value {
#if !WASI
lock.lock()
defer { lock.unlock() }
#endif
return _value
}
func set(_ newValue: Value) {
#if !WASI
lock.lock()
defer { lock.unlock() }
#endif
_value = newValue
}
func `do`(_ body: (inout Value) throws -> Void) rethrows {
#if !WASI
lock.lock()
defer { lock.unlock() }
#endif
try body(&_value)
}
}
@@ -352,6 +352,7 @@ enum StringSubscription: Subscription,
ExpressibleByStringLiteral {
case string(String)
case contains(String)
case subscription(Subscription)
init(_ subscription: Subscription) {
@@ -364,7 +365,7 @@ enum StringSubscription: Subscription,
var description: String {
switch self {
case .string(let string):
case .string(let string), .contains(let string):
return string
case .subscription(let subscription):
return String(describing: subscription)
@@ -379,7 +380,7 @@ enum StringSubscription: Subscription,
switch self {
case .subscription(let subscription):
return subscription.combineIdentifier
case .string:
case .string, .contains:
fatalError("String has no combineIdentifier")
}
}
@@ -390,7 +391,7 @@ enum StringSubscription: Subscription,
var underlying: Subscription? {
switch self {
case .string:
case .string, .contains:
return nil
case .subscription(let underlying):
return underlying
@@ -401,6 +402,25 @@ enum StringSubscription: Subscription,
@available(macOS 10.15, iOS 13.0, *)
extension StringSubscription: Equatable {
static func == (lhs: StringSubscription, rhs: StringSubscription) -> Bool {
return lhs.description == rhs.description
// swiftlint:disable pattern_matching_keywords
switch (lhs, rhs) {
case (.contains(let pattern), .subscription(let subscription)),
(.subscription(let subscription), .contains(let pattern)):
return String(describing: subscription).contains(pattern)
case (.contains(let pattern), .string(let string)),
(.string(let string), .contains(let pattern)):
return string.contains(pattern)
case let (.subscription(lhs), .subscription(rhs)):
return String(describing: lhs) == String(describing: rhs)
case (.string(let string), .subscription(let subscription)),
(.subscription(let subscription), .string(let string)):
return String(describing: subscription) == string
case let (.string(lhs), .string(rhs)):
return lhs == rhs
case let (.contains(lhs), .contains(rhs)):
return lhs.contains(rhs) || rhs.contains(lhs)
}
// swiftlint:enable pattern_matching_keywords
}
}
@@ -357,9 +357,9 @@ final class VirtualTimeScheduler: Scheduler {
_now = time
}
func executeScheduledActions(until deadline: SchedulerTimeType = .nanoseconds(.max)) {
precondition(deadline >= _now)
while let (time, action) = workQueue.min(), time <= deadline {
func executeScheduledActions(until deadline: SchedulerTimeType? = nil) {
precondition(deadline.map { $0 >= _now } ?? true)
while let (time, action) = workQueue.min(), deadline.map({ time <= $0 }) ?? true {
workQueue.extractMin()
_now = max(time, _now)
action()
@@ -0,0 +1,424 @@
//
// ObservableObjectTests.swift
//
//
// Created by kateinoigakukun on 2020/12/22.
//
import XCTest
#if swift(>=5.1)
#if OPENCOMBINE_COMPATIBILITY_TEST
import Combine
@available(macOS 10.15, iOS 13.0, *)
private typealias Published = Combine.Published
@available(macOS 10.15, iOS 13.0, *)
private typealias ObservableObject = Combine.ObservableObject
#else
import OpenCombine
private typealias Published = OpenCombine.Published
private typealias ObservableObject = OpenCombine.ObservableObject
#endif
@available(macOS 10.15, iOS 13.0, *)
final class ObservableObjectTests: XCTestCase {
var disposeBag = [AnyCancellable]()
override func tearDown() {
disposeBag = []
super.tearDown()
}
func testBasicBehavior() {
let testObject = TestObject()
var downstreamSubscription1: Subscription?
let tracking1 = TrackingSubscriberBase<Void, Never>(
receiveSubscription: { downstreamSubscription1 = $0 }
)
testObject.objectWillChange.subscribe(tracking1)
tracking1.assertHistoryEqual([.subscription("ObservableObjectPublisher")])
downstreamSubscription1?.request(.max(2))
tracking1.assertHistoryEqual([.subscription("ObservableObjectPublisher")])
testObject.state1 += 1
testObject.state1 += 2
testObject.state1 += 3
tracking1.assertHistoryEqual([.subscription("ObservableObjectPublisher"),
.signal,
.signal,
.signal])
testObject.state2 += 1
tracking1.assertHistoryEqual([.subscription("ObservableObjectPublisher"),
.signal,
.signal,
.signal,
.signal])
downstreamSubscription1?.request(.max(10))
tracking1.assertHistoryEqual([.subscription("ObservableObjectPublisher"),
.signal,
.signal,
.signal,
.signal])
let tracking2 = TrackingSubscriberBase<Void, Never>(
receiveSubscription: { $0.request(.unlimited) }
)
testObject.objectWillChange.subscribe(tracking2)
tracking2.assertHistoryEqual([.subscription("ObservableObjectPublisher")])
testObject.state1 = 42
tracking1.assertHistoryEqual([.subscription("ObservableObjectPublisher"),
.signal,
.signal,
.signal,
.signal,
.signal])
tracking2.assertHistoryEqual([.subscription("ObservableObjectPublisher"),
.signal])
downstreamSubscription1?.cancel()
testObject.state1 = -1
tracking1.assertHistoryEqual([.subscription("ObservableObjectPublisher"),
.signal,
.signal,
.signal,
.signal,
.signal])
tracking2.assertHistoryEqual([.subscription("ObservableObjectPublisher"),
.value(()),
.value(())])
}
// TODO: `objectWillChange` should return the same `ObservableObjectPublisher`
// every time for Combine compatibility
//
// func testNoFields() {
// let observableObject = NoFields()
// let publisher1 = observableObject.objectWillChange
// let publisher2 = observableObject.objectWillChange
// XCTAssert(publisher1 === publisher2)
// }
// func testNoPublishedFields() {
// let observableObject = NoPublishedFields()
// let publisher1 = observableObject.objectWillChange
// let publisher2 = observableObject.objectWillChange
// XCTAssert(publisher1 === publisher2)
// }
func testPublishedFieldIsConstant() {
let observableObject = PublishedFieldIsConstant()
let publisher1 = observableObject.objectWillChange
let publisher2 = observableObject.objectWillChange
XCTAssert(publisher1 === publisher2,
"""
Even if the Published field is a constant, a publisher \
should be installed there.
""")
}
func testDerivedClassWithPublishedField() {
let observableObject = ObservedDerivedWithObservedBase()
var counter = 0
observableObject.objectWillChange.sink {
counter += 1
}.store(in: &disposeBag)
XCTAssertEqual(observableObject.publishedValue0, 0)
XCTAssertEqual(observableObject.simpleValue, "what")
XCTAssertEqual(observableObject.subclassPublished0, 0)
XCTAssertEqual(observableObject.subclassPublished1, 1)
XCTAssertEqual(observableObject.subclassPublished2, 2)
observableObject.publishedValue0 += 5
XCTAssertEqual(counter, 1)
XCTAssertEqual(observableObject.publishedValue0, 5)
Published<String>[_enclosingInstance: observableObject,
wrapped: \.simpleValue,
storage: \.publishedValue1] += "???"
XCTAssertEqual(counter, 2)
XCTAssertEqual(observableObject.simpleValue, "what")
observableObject.subclassPublished0 += 3
XCTAssertEqual(counter, 3)
XCTAssertEqual(observableObject.subclassPublished0, 3)
observableObject.subclassPublished1 += 3
XCTAssertEqual(counter, 4)
XCTAssertEqual(observableObject.subclassPublished1, 4)
observableObject.subclassPublished2 += 3
XCTAssertEqual(counter, 5)
XCTAssertEqual(observableObject.subclassPublished1, 4)
}
func testObjCClassSubclass() {
let observableObject = ObjCClassSubclass()
let publisher1 = observableObject.objectWillChange
let publisher2 = observableObject.objectWillChange
XCTAssert(publisher1 === publisher2)
}
func testResilientClassSubclass() {
let observableObject = ResilientClassSubclass()
let publisher1 = observableObject.objectWillChange
let publisher2 = observableObject.objectWillChange
XCTAssert(publisher1 === publisher2)
}
func testResilientClassSubclass2() {
let observableObject = ResilientClassSubclass2()
let publisher1 = observableObject.objectWillChange
let publisher2 = observableObject.objectWillChange
XCTAssert(publisher1 === publisher2)
}
func testGenericClass() {
let observableObject = GenericClass(123, true)
var counter = 0
observableObject.objectWillChange.sink { counter += 1 }.store(in: &disposeBag)
XCTAssertEqual(counter, 0)
XCTAssertEqual(observableObject.value1, 123)
XCTAssertEqual(observableObject.value2, true)
observableObject.value1 += 1
XCTAssertEqual(counter, 1)
XCTAssertEqual(observableObject.value1, 124)
observableObject.value2.toggle()
XCTAssertEqual(counter, 2)
XCTAssertEqual(observableObject.value2, false)
}
func testGenericSubclassOfResilientClass() {
let observableObject = ResilientClassGenericSubclass("hello", true)
var counter = 0
observableObject.objectWillChange.sink { counter += 1 }.store(in: &disposeBag)
XCTAssertEqual(counter, 0)
XCTAssertEqual(observableObject.value1, "hello")
XCTAssertEqual(observableObject.value2, true)
observableObject.value1 += "!"
XCTAssertEqual(counter, 1)
XCTAssertEqual(observableObject.value1, "hello!")
observableObject.value2.toggle()
XCTAssertEqual(counter, 2)
XCTAssertEqual(observableObject.value2, false)
}
func testGenericSubclassOfResilientClass2() {
let observableObject = ResilientClassGenericSubclass2("hello", true)
var counter = 0
observableObject.objectWillChange.sink { counter += 1 }.store(in: &disposeBag)
XCTAssertEqual(counter, 0)
XCTAssertEqual(observableObject.value1, "hello")
XCTAssertEqual(observableObject.value2, true)
observableObject.value1 += "!"
XCTAssertEqual(counter, 1)
XCTAssertEqual(observableObject.value1, "hello!")
observableObject.value2.toggle()
XCTAssertEqual(counter, 2)
XCTAssertEqual(observableObject.value2, false)
observableObject.value3.toggle()
XCTAssertEqual(counter, 3)
XCTAssertEqual(observableObject.value3, true)
}
func testObservableDerivedWithNonObservableBase() {
let observableObject = ObservedDerivedWithNonObservedBase()
var counter = 0
observableObject.objectWillChange.sink { counter += 1 }.store(in: &disposeBag)
XCTAssertEqual(counter, 0)
XCTAssertEqual(observableObject.nonObservedBaseValue0, 10)
XCTAssertEqual(observableObject.nonObservedBaseValue1, .pi)
XCTAssertEqual(observableObject.observedDerivedValue2,
"Asuka is obviously the best girl.")
XCTAssertEqual(observableObject.observedDerivedValue3, 255)
observableObject.nonObservedBaseValue0 -= 1
XCTAssertEqual(counter, 1)
XCTAssertEqual(observableObject.nonObservedBaseValue0, 9)
observableObject.nonObservedBaseValue1 *= 2
XCTAssertEqual(counter, 2)
XCTAssertEqual(observableObject.nonObservedBaseValue1, 2 * .pi)
observableObject.observedDerivedValue2 = "Nevermind."
XCTAssertEqual(counter, 3)
XCTAssertEqual(observableObject.observedDerivedValue2, "Nevermind.")
observableObject.observedDerivedValue3 &+= 1
XCTAssertEqual(counter, 4)
XCTAssertEqual(observableObject.observedDerivedValue3, 0)
}
func testNSObjectSubclass() {
let observableObject = NSObjectSubclass()
var counter = 0
observableObject.objectWillChange.sink { counter += 1 }.store(in: &disposeBag)
XCTAssertEqual(counter, 0)
XCTAssertEqual(observableObject.value0, 0)
XCTAssertEqual(observableObject.value1, 42)
observableObject.value0 += 1
XCTAssertEqual(counter, 1)
XCTAssertEqual(observableObject.value0, 1)
observableObject.value1 += 1
XCTAssertEqual(counter, 2)
XCTAssertEqual(observableObject.value1, 43)
}
}
@available(macOS 10.15, iOS 13.0, *)
private final class NoFields: ObservableObject {}
@available(macOS 10.15, iOS 13.0, *)
private final class NoPublishedFields: ObservableObject {
var field = NoFields()
var int = 0
}
@available(macOS 10.15, iOS 13.0, *)
private final class PublishedFieldIsConstant: ObservableObject {
let publishedValue = Published(initialValue: 42)
}
@available(macOS 10.15, iOS 13.0, *)
private class ObservedBase: ObservableObject {
@Published var publishedValue0 = 0
var publishedValue1 = Published(initialValue: "Hello!")
let publishedValue2 = Published(initialValue: 42)
var simpleValue = "what"
}
@available(macOS 10.15, iOS 13.0, *)
private final class ObservedDerivedWithObservedBase: ObservedBase {
@Published var subclassPublished0 = 0
@Published var subclassPublished1 = 1
@Published var subclassPublished2 = 2
}
@available(macOS 10.15, iOS 13.0, *)
extension NSNumber: ObservableObject {}
@available(macOS 10.15, iOS 13.0, *)
private final class ObjCClassSubclass: NSObject, ObservableObject {
@Published var published = 10
}
@available(macOS 10.15, iOS 13.0, *)
private class ResilientClassSubclass: JSONDecoder, ObservableObject {
@Published var published0 = 10
@Published var published1 = "hello!"
}
@available(macOS 10.15, iOS 13.0, *)
private final class ResilientClassSubclass2: ResilientClassSubclass {
@Published var published3 = true
}
@available(macOS 10.15, iOS 13.0, *)
extension JSONEncoder: ObservableObject {}
@available(macOS 10.15, iOS 13.0, *)
private final class GenericClass<Value1, Value2>: ObservableObject {
@Published var value1: Value1
@Published var value2: Value2
init(_ value1: Value1, _ value2: Value2) {
self.value1 = value1
self.value2 = value2
}
}
@available(macOS 10.15, iOS 13.0, *)
private class NonObservedBase {
@Published var nonObservedBaseValue0 = 10
@Published var nonObservedBaseValue1 = Double.pi
}
@available(macOS 10.15, iOS 13.0, *)
private class ObservedDerivedWithNonObservedBase: NonObservedBase, ObservableObject {
@Published var observedDerivedValue2 = "Asuka is obviously the best girl."
@Published var observedDerivedValue3: UInt8 = 255
}
@available(macOS 10.15, iOS 13.0, *)
private class NSObjectSubclass: NSObject, ObservableObject {
@Published var value0 = 0
@Published var value1: UInt8 = 42
}
@available(macOS 10.15, iOS 13.0, *)
private class ResilientClassGenericSubclass<Value1, Value2>
: JSONDecoder,
ObservableObject
{
@Published var value1: Value1
@Published var value2: Value2
init(_ value1: Value1, _ value2: Value2) {
self.value1 = value1
self.value2 = value2
}
}
@available(macOS 10.15, iOS 13.0, *)
private final class ResilientClassGenericSubclass2<Value1, Value2>
: ResilientClassGenericSubclass<Value1, Value2>
{
@Published var value3 = false
}
@available(macOS 10.15, iOS 13.0, *)
private final class TestObject: ObservableObject {
@Published var state1: Int
@Published var state2: Int
var nonPublished: Int
init(_ initialValue: Int = 0) {
_state1 = Published(initialValue: initialValue)
_state2 = Published(initialValue: initialValue)
nonPublished = initialValue
}
}
#endif
@@ -110,6 +110,7 @@ final class PublishedTests: XCTestCase {
tracking1.assertHistoryEqual([.subscription("ObservableObjectPublisher")])
}
@available(macOS 11.0, iOS 14.0, *)
func testAssignToPublished() throws {
let subscription = CustomSubscription()
let publisher = CustomPublisherBase<Int, Never>(subscription: subscription)
@@ -148,6 +149,7 @@ final class PublishedTests: XCTestCase {
.cancelled])
}
@available(macOS 11.0, iOS 14.0, *)
func testAssignToPublishedFinish() {
let subscription = CustomSubscription()
let publisher = CustomPublisherBase<Int, Never>(subscription: subscription)
@@ -289,6 +291,7 @@ final class PublishedTests: XCTestCase {
)
}
@available(macOS 11.0, iOS 14.0, *)
func testProjectedValueSetter() {
let testObject1 = TestObject(1)
let testObject2 = TestObject(2)
@@ -41,14 +41,21 @@ final class AllSatisfyTests: XCTestCase {
)
}
func testAllSatisfyUpstreamFinishesImmediately() {
ReduceTests.testUpstreamFinishesImmediately(
func testAllSatisfyUpstreamFinishesImmediatelyWithDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
expectedSubscription: "AllSatisfy",
expectedResult: true,
{ $0.allSatisfy(shouldNotBeCalled()) }
)
}
func testAllSatisfyUpstreamFinishesImmediatelyWithoutDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
expectedSubscription: "AllSatisfy",
{ $0.allSatisfy(shouldNotBeCalled()) }
)
}
func testAllSatisfyCancelAlreadyCancelled() throws {
try ReduceTests.testCancelAlreadyCancelled {
$0.allSatisfy(shouldNotBeCalled())
@@ -97,13 +104,14 @@ final class AllSatisfyTests: XCTestCase {
func testAllSatisfyCancelBeforeSubscription() {
testCancelBeforeSubscription(inputType: Int.self,
shouldCrash: false,
expected: .history([.requested(.unlimited)]),
{ $0.allSatisfy(shouldNotBeCalled()) })
}
func testAllSatisfyLifecycle() throws {
try testLifecycle(sendValue: 31,
cancellingSubscriptionReleasesSubscriber: false,
finishingIsPassedThrough: false,
{ $0.allSatisfy { _ in true } })
}
@@ -155,14 +163,21 @@ final class AllSatisfyTests: XCTestCase {
)
}
func testTryAllSatisfyUpstreamFinishesImmediately() {
ReduceTests.testUpstreamFinishesImmediately(
func testTryAllSatisfyUpstreamFinishesImmediatelyWithDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
expectedSubscription: "TryAllSatisfy",
expectedResult: true,
{ $0.tryAllSatisfy(shouldNotBeCalled()) }
)
}
func testTryAllSatisfyUpstreamFinishesImmediatelyWithoutDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
expectedSubscription: "TryAllSatisfy",
{ $0.tryAllSatisfy(shouldNotBeCalled()) }
)
}
func testTryAllSatisfyCancelAlreadyCancelled() throws {
try ReduceTests.testCancelAlreadyCancelled {
$0.tryAllSatisfy(shouldNotBeCalled())
@@ -217,13 +232,14 @@ final class AllSatisfyTests: XCTestCase {
func testTryAllSatisfyCancelBeforeSubscription() {
testCancelBeforeSubscription(inputType: Int.self,
shouldCrash: false,
expected: .history([.requested(.unlimited)]),
{ $0.tryAllSatisfy(shouldNotBeCalled()) })
}
func testTryAllSatisfyLifecycle() throws {
try testLifecycle(sendValue: 31,
cancellingSubscriptionReleasesSubscriber: false,
finishingIsPassedThrough: false,
{ $0.tryAllSatisfy { _ in true } })
}
@@ -5,6 +5,8 @@
// Created by Sergej Jaskiewicz on 03.12.2019.
//
#if !WASI
import XCTest
#if OPENCOMBINE_COMPATIBILITY_TEST
@@ -170,3 +172,5 @@ final class BreakpointTests: XCTestCase {
{ $0.breakpointOnError() })
}
}
#endif // !WASI
@@ -918,10 +918,23 @@ final class BufferTests: XCTestCase {
prefetch: Publishers.PrefetchStrategy,
whenFull: Publishers.BufferingStrategy<Never>
) {
testCancelBeforeSubscription(
inputType: Int.self,
shouldCrash: false,
{ $0.buffer(size: 2, prefetch: prefetch, whenFull: whenFull) }
)
switch prefetch {
case .byRequest:
testCancelBeforeSubscription(
inputType: Int.self,
expected: .history([.requested(.unlimited)]),
{ $0.buffer(size: 2, prefetch: prefetch, whenFull: whenFull) }
)
case .keepFull:
testCancelBeforeSubscription(
inputType: Int.self,
expected: .history([.requested(.max(2))]),
{ $0.buffer(size: 2, prefetch: prefetch, whenFull: whenFull) }
)
#if OPENCOMBINE_COMPATIBILITY_TEST
@unknown default:
unreachable()
#endif
}
}
}
@@ -63,7 +63,7 @@ final class CatchTests: XCTestCase {
func testCatchReceiveValueBeforeSubscription() {
testReceiveValueBeforeSubscription(value: 1, expected: .crash) {
$0.catch { _ in Just(13) }
$0.catch(fromNever(Just<Int>.self))
}
}
@@ -71,7 +71,7 @@ final class CatchTests: XCTestCase {
testReceiveCompletionBeforeSubscription(
inputType: Int.self,
expected: .history([]),
{ $0.catch { _ in Just(13) } }
{ $0.catch(fromNever(Just<Int>.self)) }
)
}
@@ -220,7 +220,7 @@ final class CatchTests: XCTestCase {
func testTryCatchReceiveValueBeforeSubscription() {
testReceiveValueBeforeSubscription(value: 1, expected: .crash) {
$0.tryCatch { _ in Just(13) }
$0.tryCatch(fromNever(Just<Int>.self))
}
}
@@ -228,7 +228,7 @@ final class CatchTests: XCTestCase {
testReceiveCompletionBeforeSubscription(
inputType: Int.self,
expected: .history([]),
{ $0.tryCatch { _ in Just(13) } }
{ $0.tryCatch(fromNever(Just<Int>.self)) }
)
}
@@ -197,7 +197,7 @@ final class CollectByCountTests: XCTestCase {
func testCollectByCountCancelBeforeSubscription() {
testCancelBeforeSubscription(inputType: Int.self,
shouldCrash: false,
expected: .history([]),
{ $0.collect(19) })
}
@@ -27,10 +27,19 @@ final class CollectTests: XCTestCase {
{ $0.collect() })
}
func testtestUpstreamFinishesImmediately() {
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "Collect",
expectedResult: [Int](),
{ $0.collect() })
func testUpstreamFinishesImmediatelyWithDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
expectedSubscription: "Collect",
expectedResult: [Int](),
{ $0.collect() }
)
}
func testUpstreamFinishesImmediatelyWithoutDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
expectedSubscription: "Collect",
{ $0.collect() }
)
}
func testCancelAlreadyCancelled() throws {
@@ -70,13 +79,14 @@ final class CollectTests: XCTestCase {
func testCollectCancelBeforeSubscription() {
testCancelBeforeSubscription(inputType: Int.self,
shouldCrash: false,
expected: .history([.requested(.unlimited)]),
{ $0.collect() })
}
func testCollectLifecycle() throws {
try testLifecycle(sendValue: 31,
cancellingSubscriptionReleasesSubscriber: false,
finishingIsPassedThrough: false,
{ $0.collect() })
}
@@ -336,7 +336,7 @@ final class CompactMapTests: XCTestCase {
func testTryCompactMapCancelBeforeSubscription() {
testCancelBeforeSubscription(inputType: Int.self,
shouldCrash: false,
expected: .history([.cancelled]),
{ $0.tryCompactMap(shouldNotBeCalled()) })
}
@@ -64,22 +64,52 @@ final class ComparisonTests: XCTestCase {
{ $0.min(by: shouldNotBeCalled()) })
}
func testComparisonUpstreamFinishesImmediately() {
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "Comparison",
expectedResult: nil,
{ $0.max() })
func testComparisonUpstreamFinishesImmediatelyWithDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
expectedSubscription: "Comparison",
expectedResult: nil,
{ $0.max() }
)
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "Comparison",
expectedResult: nil,
{ $0.min() })
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
expectedSubscription: "Comparison",
expectedResult: nil,
{ $0.min() }
)
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "Comparison",
expectedResult: nil,
{ $0.max(by: shouldNotBeCalled()) })
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
expectedSubscription: "Comparison",
expectedResult: nil,
{ $0.max(by: shouldNotBeCalled()) }
)
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "Comparison",
expectedResult: nil,
{ $0.min(by: shouldNotBeCalled()) })
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
expectedSubscription: "Comparison",
expectedResult: nil,
{ $0.min(by: shouldNotBeCalled()) }
)
}
func testComparisonUpstreamFinishesImmediatelyWithoutDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
expectedSubscription: "Comparison",
{ $0.max() }
)
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
expectedSubscription: "Comparison",
{ $0.min() }
)
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
expectedSubscription: "Comparison",
{ $0.max(by: shouldNotBeCalled()) }
)
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
expectedSubscription: "Comparison",
{ $0.min(by: shouldNotBeCalled()) }
)
}
func testComparisonCancelAlreadyCancelled() throws {
@@ -185,29 +215,31 @@ final class ComparisonTests: XCTestCase {
func testComparisonCancelBeforeSubscription() {
testCancelBeforeSubscription(inputType: Int.self,
shouldCrash: false,
expected: .history([.requested(.unlimited)]),
{ $0.min() })
testCancelBeforeSubscription(inputType: Int.self,
shouldCrash: false,
expected: .history([.requested(.unlimited)]),
{ $0.max() })
testCancelBeforeSubscription(inputType: Int.self,
shouldCrash: false,
expected: .history([.requested(.unlimited)]),
{ $0.min(by: shouldNotBeCalled()) })
testCancelBeforeSubscription(inputType: Int.self,
shouldCrash: false,
expected: .history([.requested(.unlimited)]),
{ $0.max(by: shouldNotBeCalled()) })
}
func testComparisonLifecycle() throws {
try testLifecycle(sendValue: 31,
cancellingSubscriptionReleasesSubscriber: false,
finishingIsPassedThrough: false,
{ $0.min(by: >) })
try testLifecycle(sendValue: 31,
cancellingSubscriptionReleasesSubscriber: false,
finishingIsPassedThrough: false,
{ $0.max(by: >) })
}
@@ -272,14 +304,30 @@ final class ComparisonTests: XCTestCase {
{ $0.tryMin(by: >) })
}
func testTryComparisonUpstreamFinishesImmediately() {
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "TryComparison",
expectedResult: nil,
{ $0.tryMax(by: >) })
func testTryComparisonUpstreamFinishesImmediatelyWithDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
expectedSubscription: "TryComparison",
expectedResult: nil,
{ $0.tryMax(by: >) }
)
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "TryComparison",
expectedResult: nil,
{ $0.tryMin(by: >) })
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
expectedSubscription: "TryComparison",
expectedResult: nil,
{ $0.tryMin(by: >) }
)
}
func testTryComparisonUpstreamFinishesImmediatelyWithoutDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
expectedSubscription: "TryComparison",
{ $0.tryMax(by: >) }
)
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
expectedSubscription: "TryComparison",
{ $0.tryMin(by: >) }
)
}
func testTryComparisonCancelAlreadyCancelled() throws {
@@ -347,21 +395,23 @@ final class ComparisonTests: XCTestCase {
func testTryComparisonCancelBeforeSubscription() {
testCancelBeforeSubscription(inputType: Int.self,
shouldCrash: false,
expected: .history([.requested(.unlimited)]),
{ $0.tryMin(by: shouldNotBeCalled()) })
testCancelBeforeSubscription(inputType: Int.self,
shouldCrash: false,
expected: .history([.requested(.unlimited)]),
{ $0.tryMax(by: shouldNotBeCalled()) })
}
func testTryComparisonLifecycle() throws {
try testLifecycle(sendValue: 31,
cancellingSubscriptionReleasesSubscriber: false,
finishingIsPassedThrough: false,
{ $0.tryMin(by: >) })
try testLifecycle(sendValue: 31,
cancellingSubscriptionReleasesSubscriber: false,
finishingIsPassedThrough: false,
{ $0.tryMax(by: >) })
}
@@ -41,11 +41,19 @@ final class ContainsTests: XCTestCase {
{ $0.contains(0) })
}
func testContainsUpstreamFinishesImmediately() {
ReduceTests
.testUpstreamFinishesImmediately(expectedSubscription: "Contains",
expectedResult: false,
{ $0.contains(0) })
func testContainsUpstreamFinishesImmediatelyWithDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
expectedSubscription: "Contains",
expectedResult: false,
{ $0.contains(0) }
)
}
func testContainsUpstreamFinishesImmediatelyWithoutDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
expectedSubscription: "Contains",
{ $0.contains(0) }
)
}
func testContainsCancelAlreadyCancelled() throws {
@@ -92,13 +100,14 @@ final class ContainsTests: XCTestCase {
func testContainsCancelBeforeSubscription() {
testCancelBeforeSubscription(inputType: Int.self,
shouldCrash: false,
expected: .history([.requested(.unlimited)]),
{ $0.contains(0) })
}
func testContainsLifecycle() throws {
try testLifecycle(sendValue: 31,
cancellingSubscriptionReleasesSubscriber: false,
finishingIsPassedThrough: false,
{ $0.contains(31) })
}
@@ -142,12 +151,18 @@ final class ContainsTests: XCTestCase {
)
}
func testContainsWhereUpstreamFinishesImmediately() {
ReduceTests
.testUpstreamFinishesImmediately(
expectedSubscription: "ContainsWhere",
expectedResult: false,
{ $0.contains(where: shouldNotBeCalled()) }
func testContainsWhereUpstreamFinishesImmediatelyWithDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
expectedSubscription: "ContainsWhere",
expectedResult: false,
{ $0.contains(where: shouldNotBeCalled()) }
)
}
func testContainsWhereUpstreamFinishesImmediatelyWithoutDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
expectedSubscription: "ContainsWhere",
{ $0.contains(where: shouldNotBeCalled()) }
)
}
@@ -199,13 +214,14 @@ final class ContainsTests: XCTestCase {
func testContainsWhereCancelBeforeSubscription() {
testCancelBeforeSubscription(inputType: Int.self,
shouldCrash: false,
expected: .history([.requested(.unlimited)]),
{ $0.contains(where: shouldNotBeCalled()) })
}
func testContainsWhereLifecycle() throws {
try testLifecycle(sendValue: 31,
cancellingSubscriptionReleasesSubscriber: false,
finishingIsPassedThrough: false,
{ $0.contains { _ in true } })
}
@@ -264,11 +280,19 @@ final class ContainsTests: XCTestCase {
)
}
func testTryContainsWhereUpstreamFinishesImmediately() {
ReduceTests .testUpstreamFinishesImmediately(
func testTryContainsWhereUpstreamFinishesImmediatelyWithDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
expectedSubscription: "TryContainsWhere",
expectedResult: false,
{ $0.tryContains(where: shouldNotBeCalled()) })
{ $0.tryContains(where: shouldNotBeCalled()) }
)
}
func testTryContainsWhereUpstreamFinishesImmediatelyWithWithoutDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
expectedSubscription: "TryContainsWhere",
{ $0.tryContains(where: shouldNotBeCalled()) }
)
}
func testTryContainsWhereCancelAlreadyCancelled() throws {
@@ -325,13 +349,14 @@ final class ContainsTests: XCTestCase {
func testTryContainsWhereCancelBeforeSubscription() {
testCancelBeforeSubscription(inputType: Int.self,
shouldCrash: false,
expected: .history([.requested(.unlimited)]),
{ $0.tryContains(where: shouldNotBeCalled()) })
}
func testTryContainsWhereLifecycle() throws {
try testLifecycle(sendValue: 31,
cancellingSubscriptionReleasesSubscriber: false,
finishingIsPassedThrough: false,
{ $0.tryContains { _ in true } })
}
@@ -27,10 +27,19 @@ final class CountTests: XCTestCase {
{ $0.count() })
}
func testtestUpstreamFinishesImmediately() {
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "Count",
expectedResult: 0,
{ $0.count() })
func testUpstreamFinishesImmediatelyWithDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
expectedSubscription: "Count",
expectedResult: 0,
{ $0.count() }
)
}
func testUpstreamFinishesImmediatelyWithoutDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
expectedSubscription: "Count",
{ $0.count() }
)
}
func testCancelAlreadyCancelled() throws {
@@ -70,13 +79,14 @@ final class CountTests: XCTestCase {
func testCountCancelBeforeSubscription() {
testCancelBeforeSubscription(inputType: Int.self,
shouldCrash: false,
expected: .history([.requested(.unlimited)]),
{ $0.count() })
}
func testCountLifecycle() throws {
try testLifecycle(sendValue: 31,
cancellingSubscriptionReleasesSubscriber: false,
finishingIsPassedThrough: false,
{ $0.count() })
}
@@ -593,7 +593,7 @@ final class DebounceTests: XCTestCase {
let scheduler = VirtualTimeScheduler()
testCancelBeforeSubscription(
inputType: Int.self,
shouldCrash: false,
expected: .history([]),
{ $0.timeout(.nanoseconds(13), scheduler: scheduler) }
)
}
@@ -440,7 +440,7 @@ final class DelayTests: XCTestCase {
}
func testDelayCancelBeforeSubscription() {
testCancelBeforeSubscription(inputType: Int.self, shouldCrash: false) {
testCancelBeforeSubscription(inputType: Int.self, expected: .history([])) {
$0.delay(for: 0.35, scheduler: ImmediateScheduler.shared)
}
}
@@ -158,7 +158,7 @@ final class DropTests: XCTestCase {
func testDropCancelBeforeSubscription() {
testCancelBeforeSubscription(inputType: Int.self,
shouldCrash: false,
expected: .history([]),
{ $0.dropFirst(0) })
}
@@ -319,7 +319,7 @@ final class DropUntilOutputTests: XCTestCase {
func testDropUntilOutputCancelBeforeSubscription() {
testCancelBeforeSubscription(inputType: Int.self,
shouldCrash: false,
expected: .history([.cancelled]),
{ $0.drop(untilOutputFrom: Empty<Int, Never>()) })
}
@@ -62,7 +62,7 @@ final class DropWhileTests: XCTestCase {
func testDropWhileCancelBeforeSubscription() {
testCancelBeforeSubscription(inputType: Int.self,
shouldCrash: false,
expected: .history([]),
{ $0.drop(while: shouldNotBeCalled()) })
}
@@ -138,7 +138,7 @@ final class DropWhileTests: XCTestCase {
func testTryDropWhileCancelBeforeSubscription() {
testCancelBeforeSubscription(inputType: Int.self,
shouldCrash: false,
expected: .history([]),
{ $0.tryDrop(while: shouldNotBeCalled()) })
}
@@ -138,7 +138,7 @@ final class EncodeTests: XCTestCase {
func testEncodeCancelBeforeSubscription() {
testCancelBeforeSubscription(inputType: Int.self,
shouldCrash: false,
expected: .history([]),
{ $0.encode(encoder: encoder) })
}
@@ -258,7 +258,7 @@ final class EncodeTests: XCTestCase {
func testDecodeCancelBeforeSubscription() {
testCancelBeforeSubscription(inputType: Int.self,
shouldCrash: false,
expected: .history([]),
{ $0.decode(type: String.self, decoder: decoder) })
}
@@ -309,7 +309,7 @@ final class FilterTests: XCTestCase {
func testTryFilterCancelBeforeSubscription() {
testCancelBeforeSubscription(inputType: Int.self,
shouldCrash: false,
expected: .history([.cancelled]),
{ $0.tryFilter(shouldNotBeCalled()) })
}
@@ -77,11 +77,19 @@ final class FirstTests: XCTestCase {
}
}
func testFirstFinishesImmediately() {
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "First",
expectedResult: nil) {
$0.first()
}
func testFirstFinishesImmediatelyWithDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
expectedSubscription: "First",
expectedResult: nil,
{ $0.first() }
)
}
func testFirstFinishesImmediatelyWithoutDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
expectedSubscription: "First",
{ $0.first() }
)
}
func testFirstRequestsUnlimitedThenSendsSubscription() {
@@ -118,13 +126,14 @@ final class FirstTests: XCTestCase {
func testFirstCancelBeforeSubscription() {
testCancelBeforeSubscription(inputType: Int.self,
shouldCrash: false,
expected: .history([.requested(.unlimited)]),
{ $0.first() })
}
func testFirstLifecycle() throws {
try testLifecycle(sendValue: 31,
cancellingSubscriptionReleasesSubscriber: false,
finishingIsPassedThrough: false,
{ $0.first() })
}
@@ -222,11 +231,19 @@ final class FirstTests: XCTestCase {
}
}
func testFirstWhereFinishesImmediately() {
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "TryFirst",
expectedResult: nil) {
$0.first(where: { $0 > 2 })
}
func testFirstWhereFinishesImmediatelyWithDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
expectedSubscription: "TryFirst",
expectedResult: nil,
{ $0.first(where: { $0 > 2 }) }
)
}
func testFirstWhereFinishesImmediatelyWithoutDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
expectedSubscription: "TryFirst",
{ $0.first(where: { $0 > 2 }) }
)
}
func testFirstWhereRequestsUnlimitedThenSendsSubscription() {
@@ -271,13 +288,14 @@ final class FirstTests: XCTestCase {
func testFirstWhereCancelBeforeSubscription() {
testCancelBeforeSubscription(inputType: Int.self,
shouldCrash: false,
expected: .history([.requested(.unlimited)]),
{ $0.first(where: shouldNotBeCalled()) })
}
func testFirstWhereLifecycle() throws {
try testLifecycle(sendValue: 31,
cancellingSubscriptionReleasesSubscriber: false,
finishingIsPassedThrough: false,
{ $0.first { $0 > 1 } })
}
@@ -369,12 +387,19 @@ final class FirstTests: XCTestCase {
}
}
func testTryFirstWhereFinishesImmediately() {
ReduceTests
.testUpstreamFinishesImmediately(expectedSubscription: "TryFirstWhere",
expectedResult: nil) {
$0.tryFirst(where: { $0 > 2 })
}
func testTryFirstWhereFinishesImmediatelyWithDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
expectedSubscription: "TryFirstWhere",
expectedResult: nil,
{ $0.tryFirst(where: { $0 > 2 }) }
)
}
func testTryFirstWhereFinishesImmediatelyWithoutDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
expectedSubscription: "TryFirstWhere",
{ $0.tryFirst(where: { $0 > 2 }) }
)
}
func testTryFirstWhereRequestsUnlimitedThenSendsSubscription() {
@@ -439,13 +464,14 @@ final class FirstTests: XCTestCase {
func testTryFirstWhereCancelBeforeSubscription() {
testCancelBeforeSubscription(inputType: Int.self,
shouldCrash: false,
expected: .history([.requested(.unlimited)]),
{ $0.tryFirst(where: shouldNotBeCalled()) })
}
func testTryFirstWhereLifecycle() throws {
try testLifecycle(sendValue: 31,
cancellingSubscriptionReleasesSubscriber: false,
finishingIsPassedThrough: false,
{ $0.tryFirst { $0 > 1 } })
}
@@ -589,7 +589,7 @@ final class FlatMapTests: XCTestCase {
createSut: { $0.flatMap(maxPublishers: .max(1)) { $0 } }
)
child.willSubscribe = { subscriber, _ in
child.willSubscribe = { _, _ in
helper.publisher.send(completion: .finished)
}
@@ -643,6 +643,31 @@ final class FlatMapTests: XCTestCase {
XCTAssertEqual(childSubscription2.history, [.requested(.unlimited)])
}
func testCrashesWhenUpstreamFailsDuringChildCancellation() {
let helper = OperatorTestHelper(
publisherType: CustomPublisherBase<CustomPublisher, TestingError>.self,
initialDemand: .unlimited,
receiveValueDemand: .none,
createSut: { $0.flatMap { $0 } }
)
let childSubscription = CustomSubscription()
let child = CustomPublisher(subscription: childSubscription)
var counter = 0
childSubscription.onCancel = {
if counter >= 5 { return }
counter += 1
helper.publisher.send(completion: .failure(.oops))
}
XCTAssertEqual(helper.publisher.send(child), .none)
assertCrashes {
helper.publisher.send(completion: .failure(.oops))
}
}
func testDoesNotCompleteWithBufferedValues() {
let upstreamPublisher = PassthroughSubject<Void, Never>()
@@ -1043,6 +1068,7 @@ final class FlatMapTests: XCTestCase {
)
}
@available(macOS 11.0, iOS 14.0, *)
func testOverloadWhenUpstreamNeverFailsButChildrenCanFail() {
let child = CustomPublisher(subscription: nil)
let helper = OperatorTestHelper(
@@ -1056,6 +1082,7 @@ final class FlatMapTests: XCTestCase {
XCTAssertEqual(helper.sut.transform(0), child)
}
@available(macOS 11.0, iOS 14.0, *)
func testOverloadWhenUpstreamCanFailButChildrenNeverFail() {
let child = CustomPublisherBase<Int, Never>(subscription: nil)
@@ -218,9 +218,10 @@ final class HandleEventsTests: XCTestCase {
func testHandleEventsCancelBeforeSubscription() {
var history = [Event<Never>]()
testCancelBeforeSubscription(inputType: Int.self,
shouldCrash: false,
expected: .history([]),
{ $0.handleAllEvents { history.append($0) } })
XCTAssertEqual(history, [.receiveCancel])
XCTAssertEqual(history, [.receiveCancel,
.receiveSubscription("CustomSubscription")])
}
func testHandleEventsReflection() throws {
@@ -256,7 +256,10 @@ final class JustTests: XCTestCase {
}
func testMapErrorOperatorSpecialization() {
XCTAssertEqual(try Sut(42).mapError { _ in TestingError.oops }.result.get(), 42)
XCTAssertEqual(
try Sut(42).mapError(fromNever(TestingError.self)).result.get(),
42
)
}
func testReplaceErrorOperatorSpecialization() {
@@ -49,11 +49,19 @@ final class LastTests: XCTestCase {
}
}
func testLastFinishesImmediately() {
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "Last",
expectedResult: nil) {
$0.last()
}
func testLastFinishesImmediatelyWithDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
expectedSubscription: "Last",
expectedResult: nil,
{ $0.last() }
)
}
func testLastFinishesImmediatelyWithoutDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
expectedSubscription: "Last",
{ $0.last() }
)
}
func testLastRequestsUnlimitedThenSendsSubscription() {
@@ -90,13 +98,14 @@ final class LastTests: XCTestCase {
func testLastCancelBeforeSubscription() {
testCancelBeforeSubscription(inputType: Int.self,
shouldCrash: false,
expected: .history([.requested(.unlimited)]),
{ $0.last() })
}
func testLastLifecycle() throws {
try testLifecycle(sendValue: 31,
cancellingSubscriptionReleasesSubscriber: false,
finishingIsPassedThrough: false,
{ $0.last() })
}
@@ -195,11 +204,19 @@ final class LastTests: XCTestCase {
}
}
func testLastWhereFinishesImmediately() {
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "LastWhere",
expectedResult: nil) {
$0.last(where: { $0 > 2 })
}
func testLastWhereFinishesImmediatelyWithDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
expectedSubscription: "LastWhere",
expectedResult: nil,
{ $0.last(where: { $0 > 2 }) }
)
}
func testLastWhereFinishesImmediatelyWithoutDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
expectedSubscription: "LastWhere",
{ $0.last(where: { $0 > 2 }) }
)
}
func testLastWhereRequestsUnlimitedThenSendsSubscription() {
@@ -244,13 +261,14 @@ final class LastTests: XCTestCase {
func testLastWhereCancelBeforeSubscription() {
testCancelBeforeSubscription(inputType: Int.self,
shouldCrash: false,
expected: .history([.requested(.unlimited)]),
{ $0.last(where: shouldNotBeCalled()) })
}
func testLastWhereLifecycle() throws {
try testLifecycle(sendValue: 31,
cancellingSubscriptionReleasesSubscriber: false,
finishingIsPassedThrough: false,
{ $0.last { $0 > 1 } })
}
@@ -350,12 +368,19 @@ final class LastTests: XCTestCase {
}
}
func testTryLastWhereFinishesImmediately() {
ReduceTests
.testUpstreamFinishesImmediately(expectedSubscription: "TryLastWhere",
expectedResult: nil) {
$0.tryLast(where: { $0 > 2 })
}
func testTryLastWhereFinishesImmediatelyWithDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
expectedSubscription: "TryLastWhere",
expectedResult: nil,
{ $0.tryLast(where: { $0 > 2 }) }
)
}
func testTryLastWhereFinishesImmediatelyWithoutDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
expectedSubscription: "TryLastWhere",
{ $0.tryLast(where: { $0 > 2 }) }
)
}
func testTryLastWhereRequestsUnlimitedThenSendsSubscription() {
@@ -420,13 +445,14 @@ final class LastTests: XCTestCase {
func testTryLastWhereCancelBeforeSubscription() {
testCancelBeforeSubscription(inputType: Int.self,
shouldCrash: false,
expected: .history([.requested(.unlimited)]),
{ $0.tryLast(where: shouldNotBeCalled()) })
}
func testTryLastWhereLifecycle() throws {
try testLifecycle(sendValue: 31,
cancellingSubscriptionReleasesSubscriber: false,
finishingIsPassedThrough: false,
{ $0.tryLast { $0 > 1 } })
}
@@ -279,7 +279,7 @@ final class MapTests: XCTestCase {
func testTryMapCancelBeforeSubscription() {
testCancelBeforeSubscription(inputType: Int.self,
shouldCrash: false,
expected: .history([]),
{ $0.tryMap(shouldNotBeCalled()) })
}
@@ -142,9 +142,11 @@ final class MeasureIntervalTests: XCTestCase {
}
func testMeasureIntervalCancelBeforeSubscription() {
testCancelBeforeSubscription(inputType: Int.self, shouldCrash: false) {
$0.measureInterval(using: ImmediateScheduler.shared)
}
testCancelBeforeSubscription(
inputType: Int.self,
expected: .history([]),
{ $0.measureInterval(using: ImmediateScheduler.shared) }
)
}
func testMeasureIntervalReflection() throws {
@@ -13,7 +13,7 @@ import Combine
import OpenCombine
#endif
@available(macOS 10.15, iOS 13.0, *)
@available(macOS 11.0, iOS 14.0, *)
final class OptionalPublisherTests: XCTestCase {
private typealias Sut<Output> = OptionalPublisher<Output>
@@ -338,8 +338,6 @@ final class OptionalPublisherTests: XCTestCase {
XCTAssertEqual(Sut<Int>(12).output(in: ...0), Sut(12))
XCTAssertEqual(Sut<Int>(12).output(in: ..<0), Sut(nil))
XCTAssertEqual(Sut<Int>(12).output(in: ..<1), Sut(12))
XCTAssertEqual(Sut<Int>(12).output(in: Range(uncheckedBounds: (0, -1))), Sut(12))
XCTAssertEqual(Sut<Int>(12).output(in: Range(uncheckedBounds: (1, -1))), Sut(nil))
let trackingRange = TrackingRangeExpression(0 ..< 10)
_ = Sut<Int>(12).output(in: trackingRange)
@@ -385,10 +383,10 @@ final class OptionalPublisherTests: XCTestCase {
}
#if OPENCOMBINE_COMPATIBILITY_TEST || !canImport(Combine)
@available(macOS 10.15, iOS 13.0, *)
@available(macOS 11.0, iOS 14.0, *)
typealias OptionalPublisher<Output> = Optional<Output>.Publisher
@available(macOS 10.15, iOS 13.0, *)
@available(macOS 11.0, iOS 14.0, *)
func makePublisher<Output>(_ optional: Output?) -> OptionalPublisher<Output> {
return optional.publisher
}
@@ -202,7 +202,7 @@ final class OutputTests: XCTestCase {
func testOutputCancelBeforeSubscription() {
testCancelBeforeSubscription(inputType: Int.self,
shouldCrash: false,
expected: .history([]),
{ $0.output(in: 0 ..< 3) })
}
@@ -0,0 +1,386 @@
//
// PrefixUntilOutputTests.swift
//
//
// Created by Sergej Jaskiewicz on 08.11.2020.
//
import XCTest
#if OPENCOMBINE_COMPATIBILITY_TEST
import Combine
#else
import OpenCombine
#endif
@available(macOS 10.15, iOS 13.0, *)
final class PrefixUntilOutputTests: XCTestCase {
func testBasicBehavior() {
let terminatingSubscription = CustomSubscription()
let terminatingPublisher = CustomPublisher(subscription: terminatingSubscription)
let helper = OperatorTestHelper(
publisherType: CustomPublisher.self,
initialDemand: .max(3),
receiveValueDemand: .max(2),
createSut: { $0.prefix(untilOutputFrom: terminatingPublisher) }
)
XCTAssertEqual(helper.tracking.history,
[.subscription(.contains("PrefixUntilOutput"))])
XCTAssertEqual(helper.subscription.history, [.requested(.max(3))])
XCTAssertEqual(terminatingSubscription.history, [.requested(.max(1))])
XCTAssertEqual(helper.publisher.send(1), .max(2))
XCTAssertEqual(helper.publisher.send(2), .max(2))
XCTAssertEqual(terminatingPublisher.send(1000), .none)
XCTAssertEqual(helper.tracking.history,
[.subscription(.contains("PrefixUntilOutput")),
.value(1),
.value(2),
.completion(.finished)])
XCTAssertEqual(helper.subscription.history, [.requested(.max(3)), .cancelled])
XCTAssertEqual(terminatingSubscription.history, [.requested(.max(1))])
XCTAssertEqual(helper.publisher.send(3), .none)
XCTAssertEqual(terminatingPublisher.send(1001), .none)
XCTAssertEqual(helper.tracking.history,
[.subscription(.contains("PrefixUntilOutput")),
.value(1),
.value(2),
.completion(.finished)])
XCTAssertEqual(helper.subscription.history, [.requested(.max(3)), .cancelled])
XCTAssertEqual(terminatingSubscription.history, [.requested(.max(1))])
helper.publisher.send(completion: .finished)
helper.publisher.send(completion: .failure(.oops))
XCTAssertEqual(helper.tracking.history,
[.subscription(.contains("PrefixUntilOutput")),
.value(1),
.value(2),
.completion(.finished)])
XCTAssertEqual(helper.subscription.history, [.requested(.max(3)), .cancelled])
XCTAssertEqual(terminatingSubscription.history, [.requested(.max(1))])
}
func testCombineIdentifiers() {
let terminatingSubscription = CustomSubscription()
let terminatingPublisher = CustomPublisher(subscription: terminatingSubscription)
let helper = OperatorTestHelper(
publisherType: CustomPublisher.self,
initialDemand: .max(3),
receiveValueDemand: .max(2),
createSut: { $0.prefix(untilOutputFrom: terminatingPublisher) }
)
XCTAssertEqual(terminatingPublisher.subscriber?.combineIdentifier,
helper.publisher.subscriber?.combineIdentifier)
}
func testRequestZeroDemand() throws {
let terminatingSubscription = CustomSubscription()
let terminatingPublisher = CustomPublisher(subscription: terminatingSubscription)
let helper = OperatorTestHelper(
publisherType: CustomPublisher.self,
initialDemand: nil,
receiveValueDemand: .max(2),
createSut: { $0.prefix(untilOutputFrom: terminatingPublisher) }
)
try XCTUnwrap(helper.downstreamSubscription).request(.none)
XCTAssertEqual(helper.subscription.history, [.requested(.none)])
}
func testCancellation() throws {
let terminatingSubscription = CustomSubscription()
let terminatingPublisher = CustomPublisher(subscription: terminatingSubscription)
let helper = OperatorTestHelper(
publisherType: CustomPublisher.self,
initialDemand: nil,
receiveValueDemand: .max(2),
createSut: { $0.prefix(untilOutputFrom: terminatingPublisher) }
)
terminatingSubscription.onCancel = {
XCTAssertEqual(helper.subscription.history,
[.cancelled],
"Upstream subscription should be cancelled first")
}
try XCTUnwrap(helper.downstreamSubscription).cancel()
XCTAssertEqual(helper.subscription.history, [.cancelled])
XCTAssertEqual(terminatingSubscription.history,
[.requested(.max(1)), .cancelled])
try XCTUnwrap(helper.downstreamSubscription).request(.max(42))
try XCTUnwrap(helper.downstreamSubscription).cancel()
XCTAssertEqual(helper.subscription.history, [.cancelled])
XCTAssertEqual(terminatingSubscription.history,
[.requested(.max(1)), .cancelled])
}
func testUpstreamCompletion() {
let terminatingSubscription = CustomSubscription()
let terminatingPublisher = CustomPublisher(subscription: terminatingSubscription)
let helper = OperatorTestHelper(
publisherType: CustomPublisher.self,
initialDemand: nil,
receiveValueDemand: .max(2),
createSut: { $0.prefix(untilOutputFrom: terminatingPublisher) }
)
helper.tracking.onFinish = {
XCTAssertEqual(terminatingSubscription.history, [.requested(.max(1)),
.cancelled])
}
helper.publisher.send(completion: .finished)
XCTAssertEqual(helper.tracking.history,
[.subscription(.contains("PrefixUntilOutput")),
.completion(.finished)])
XCTAssertEqual(helper.subscription.history, [])
XCTAssertEqual(terminatingSubscription.history, [.requested(.max(1)),
.cancelled])
}
func testCancelsUpstreamWhenTerminatorSendsValue() {
let terminatingSubscription = CustomSubscription()
let terminatingPublisher = CustomPublisher(subscription: terminatingSubscription)
let helper = OperatorTestHelper(
publisherType: CustomPublisher.self,
initialDemand: nil,
receiveValueDemand: .max(2),
createSut: { $0.prefix(untilOutputFrom: terminatingPublisher) }
)
helper.tracking.onFinish = {
XCTAssertEqual(helper.subscription.history, [.cancelled])
}
XCTAssertEqual(terminatingPublisher.send(42), .none)
XCTAssertEqual(helper.tracking.history,
[.subscription(.contains("PrefixUntilOutput")),
.completion(.finished)])
XCTAssertEqual(helper.subscription.history, [.cancelled])
XCTAssertEqual(terminatingSubscription.history, [.requested(.max(1))])
}
func testTerminatorFinishesWithoutProducingValues() {
testTerminatorCompletesWithoutProducingValues(completion: .finished)
}
func testTerminatorFailsWithoutProducingValues() {
testTerminatorCompletesWithoutProducingValues(completion: .failure(.oops))
}
private func testTerminatorCompletesWithoutProducingValues(
completion: Subscribers.Completion<TestingError>
) {
let terminatingSubscription = CustomSubscription()
let terminatingPublisher = CustomPublisher(subscription: terminatingSubscription)
let helper = OperatorTestHelper(
publisherType: CustomPublisher.self,
initialDemand: nil,
receiveValueDemand: .max(2),
createSut: { $0.prefix(untilOutputFrom: terminatingPublisher) }
)
terminatingPublisher.send(completion: completion)
XCTAssertEqual(helper.subscription.history, [])
XCTAssertEqual(helper.tracking.history,
[.subscription(.contains("PrefixUntilOutput"))])
XCTAssertEqual(terminatingSubscription.history, [.requested(.max(1))])
XCTAssertEqual(terminatingPublisher.send(42), .none)
terminatingPublisher.send(completion: .finished)
terminatingPublisher.send(completion: .failure(.oops))
XCTAssertEqual(helper.publisher.send(1), .max(2))
XCTAssertEqual(helper.subscription.history, [])
XCTAssertEqual(helper.tracking.history,
[.subscription(.contains("PrefixUntilOutput")), .value(1)])
XCTAssertEqual(terminatingSubscription.history, [.requested(.max(1))])
}
func testTerminatorEmitsValueBeforeUpstreamSendsSubscription() {
let terminatingSubscription = CustomSubscription()
let terminatingPublisher = CustomPublisher(subscription: terminatingSubscription)
let upstream = CustomPublisher(subscription: nil)
let tracking = TrackingSubscriber()
upstream.prefix(untilOutputFrom: terminatingPublisher).subscribe(tracking)
XCTAssertEqual(tracking.history, [])
XCTAssertEqual(terminatingSubscription.history, [.requested(.max(1))])
XCTAssertEqual(terminatingPublisher.send(-1), .none)
XCTAssertEqual(tracking.history, [.completion(.finished)])
XCTAssertEqual(terminatingSubscription.history, [.requested(.max(1))])
let subscription = CustomSubscription()
upstream.send(subscription: subscription)
XCTAssertEqual(subscription.history, [.cancelled])
XCTAssertEqual(tracking.history, [.completion(.finished)])
XCTAssertEqual(terminatingSubscription.history, [.requested(.max(1))])
}
func testPrefixUntilOutputReceiveValueBeforeSubscription() {
let terminatingSubscription = CustomSubscription()
let terminatingPublisher = CustomPublisher(subscription: terminatingSubscription)
testReceiveValueBeforeSubscription(
value: 31,
expected: .history([], demand: .none),
{ $0.prefix(untilOutputFrom: terminatingPublisher) }
)
XCTAssertEqual(terminatingSubscription.history, [.requested(.max(1))])
let subscription = CustomSubscription()
let publisher = CustomPublisher(subscription: subscription)
testReceiveValueBeforeSubscription(
value: 31,
expected: .history([.subscription(.contains("PrefixUntilOutput"))],
demand: .none),
{ publisher.prefix(untilOutputFrom: $0) }
)
XCTAssertEqual(subscription.history, [])
}
func testPrefixUntilOutputReceiveCompletionBeforeSubscription() {
let terminatingSubscription = CustomSubscription()
let terminatingPublisher = CustomPublisher(subscription: terminatingSubscription)
testReceiveCompletionBeforeSubscription(
inputType: Int.self,
expected: .history([]),
{ $0.prefix(untilOutputFrom: terminatingPublisher) }
)
XCTAssertEqual(terminatingSubscription.history, [.requested(.max(1)), .cancelled])
let subscription = CustomSubscription()
let publisher = CustomPublisher(subscription: subscription)
testReceiveCompletionBeforeSubscription(
inputType: Int.self,
expected: .history([.subscription(.contains("PrefixUntilOutput"))]),
{ publisher.prefix(untilOutputFrom: $0) }
)
XCTAssertEqual(subscription.history, [])
}
func testPrefixUntilOutputRequestBeforeSubscription() {
let terminatingSubscription = CustomSubscription()
let terminatingPublisher = CustomPublisher(subscription: terminatingSubscription)
testRequestBeforeSubscription(
inputType: Int.self,
shouldCrash: false,
{ $0.prefix(untilOutputFrom: terminatingPublisher) }
)
XCTAssertEqual(terminatingSubscription.history, [.requested(.max(1))])
}
func testPrefixUntilOutputCancelBeforeSubscription() {
let terminatingSubscription = CustomSubscription()
let terminatingPublisher = CustomPublisher(subscription: terminatingSubscription)
testCancelBeforeSubscription(
inputType: Int.self,
expected: .history([.cancelled]),
{ $0.prefix(untilOutputFrom: terminatingPublisher) }
)
XCTAssertEqual(terminatingSubscription.history, [.requested(.max(1)), .cancelled])
}
func testPrefixUntilOutputReceiveSubscriptionTwice() throws {
let terminatingSubscription = CustomSubscription()
let terminatingPublisher = CustomPublisher(subscription: terminatingSubscription)
try testReceiveSubscriptionTwice {
$0.prefix(untilOutputFrom: terminatingPublisher)
}
XCTAssertEqual(terminatingSubscription.history, [.requested(.max(1)), .cancelled])
do {
let subscription = CustomSubscription()
let publisher = CustomPublisher(subscription: subscription)
let helper = OperatorTestHelper(
publisherType: CustomPublisher.self,
initialDemand: nil,
receiveValueDemand: .none,
createSut: { publisher.prefix(untilOutputFrom: $0) }
)
XCTAssertEqual(helper.subscription.history, [.requested(.max(1))])
let secondSubscription = CustomSubscription()
try XCTUnwrap(helper.publisher.subscriber)
.receive(subscription: secondSubscription)
XCTAssertEqual(secondSubscription.history, [.cancelled])
try XCTUnwrap(helper.publisher.subscriber)
.receive(subscription: helper.subscription)
XCTAssertEqual(helper.subscription.history, [.requested(.max(1)),
.cancelled])
try XCTUnwrap(helper.downstreamSubscription).cancel()
XCTAssertEqual(helper.subscription.history, [.requested(.max(1)),
.cancelled,
.cancelled])
XCTAssertEqual(subscription.history, [.cancelled])
}
}
func testPrefixUntilOutputReflection() throws {
// PrefixUntilOutput's Inner doesn't customize its reflection
let terminatingPublisher = CustomPublisher(subscription: CustomSubscription())
try testReflection(parentInput: Int.self,
parentFailure: Error.self,
description: nil,
customMirror: nil,
playgroundDescription: nil) {
$0.prefix(untilOutputFrom: terminatingPublisher)
}
let publisher = CustomPublisher(subscription: CustomSubscription())
try testReflection(parentInput: Int.self,
parentFailure: Error.self,
description: nil,
customMirror: nil,
playgroundDescription: nil) {
publisher.prefix(untilOutputFrom: $0)
}
}
func testPrefixUntilOutputLifecycle() throws {
let terminatingPublisher = CustomPublisher(subscription: CustomSubscription())
try testLifecycle(sendValue: 31,
cancellingSubscriptionReleasesSubscriber: false,
finishingIsPassedThrough: false,
{ $0.prefix(untilOutputFrom: terminatingPublisher) })
let publisher = CustomPublisher(subscription: CustomSubscription())
try testLifecycle(sendValue: 31,
cancellingSubscriptionReleasesSubscriber: false,
finishingIsPassedThrough: false,
{ publisher.prefix(untilOutputFrom: $0) })
}
}
@@ -91,7 +91,7 @@ final class PrefixWhileTests: XCTestCase {
func testPrefixWhileCancelBeforeSubscription() {
testCancelBeforeSubscription(inputType: Int.self,
shouldCrash: false,
expected: .history([.cancelled]),
{ $0.prefix(while: shouldNotBeCalled()) })
}
@@ -231,7 +231,7 @@ final class PrefixWhileTests: XCTestCase {
func testTryPrefixWhileCancelBeforeSubscription() {
testCancelBeforeSubscription(inputType: Int.self,
shouldCrash: false,
expected: .history([.cancelled]),
{ $0.tryPrefix(while: shouldNotBeCalled()) })
}
@@ -138,7 +138,7 @@ final class PrintTests: XCTestCase {
func testPrintCancelBeforeSubscription() {
testCancelBeforeSubscription(inputType: Int.self,
shouldCrash: false,
expected: .history([]),
{ $0.print() })
}
@@ -405,7 +405,6 @@ final class PrintTests: XCTestCase {
}
private final class HistoryStream: TextOutputStream {
let output = Atomic([String]())
func write(_ string: String) {
@@ -366,7 +366,7 @@ final class ReceiveOnTests: XCTestCase {
}
func testReceiveOnCancelBeforeSubscription() {
testCancelBeforeSubscription(inputType: Int.self, shouldCrash: false) {
testCancelBeforeSubscription(inputType: Int.self, expected: .history([])) {
$0.receive(on: ImmediateScheduler.shared)
}
}
@@ -30,11 +30,19 @@ final class ReduceTests: XCTestCase {
}
}
func testReduceFinishesImmediately() {
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "Reduce",
expectedResult: 1) {
$0.reduce(1, *)
}
func testReduceFinishesImmediatelyWithDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
expectedSubscription: "Reduce",
expectedResult: 1,
{ $0.reduce(1, *) }
)
}
func testReduceFinishesImmediatelyWithoutDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
expectedSubscription: "Reduce",
{ $0.reduce(1, *) }
)
}
func testReduceRequestsUnlimitedThenSendsSubscription() {
@@ -71,13 +79,14 @@ final class ReduceTests: XCTestCase {
func testReduceCancelBeforeSubscription() {
testCancelBeforeSubscription(inputType: Int.self,
shouldCrash: false,
expected: .history([.requested(.unlimited)]),
{ $0.reduce(0, shouldNotBeCalled()) })
}
func testReduceLifecycle() throws {
try testLifecycle(sendValue: 42,
cancellingSubscriptionReleasesSubscriber: false,
finishingIsPassedThrough: false,
{ $0.reduce(0, +) })
}
@@ -118,11 +127,19 @@ final class ReduceTests: XCTestCase {
}
}
func testTryReduceFinishesImmediately() {
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "TryReduce",
expectedResult: 1) {
$0.tryReduce(1, *)
}
func testTryReduceFinishesImmediatelyWithDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
expectedSubscription: "TryReduce",
expectedResult: 1,
{ $0.tryReduce(1, *) }
)
}
func testTryReduceFinishesImmediatelyWithoutDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
expectedSubscription: "TryReduce",
{ $0.tryReduce(1, *) }
)
}
func testTryReduceRequestsUnlimitedThenSendsSubscription() {
@@ -165,13 +182,14 @@ final class ReduceTests: XCTestCase {
func testTryReduceCancelBeforeSubscription() {
testCancelBeforeSubscription(inputType: Int.self,
shouldCrash: false,
expected: .history([.requested(.unlimited)]),
{ $0.tryReduce(0, shouldNotBeCalled()) })
}
func testTryReduceLifecycle() throws {
try testLifecycle(sendValue: 42,
cancellingSubscriptionReleasesSubscriber: false,
finishingIsPassedThrough: false,
{ $0.tryReduce(0, +) })
}
@@ -306,7 +324,7 @@ final class ReduceTests: XCTestCase {
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
}
static func testUpstreamFinishesImmediately<Operator: Publisher>(
static func testUpstreamFinishesImmediatelyWithDemand<Operator: Publisher>(
expectedSubscription: StringSubscription,
expectedResult: Operator.Output?,
_ makeOperator: (CustomPublisherBase<Int, Error>) -> Operator
@@ -314,7 +332,7 @@ final class ReduceTests: XCTestCase {
let helper = OperatorTestHelper(
publisherType: CustomPublisherBase<Int, Error>.self,
initialDemand: nil, // Downstream should receive the result nonetheless
initialDemand: .max(1),
receiveValueDemand: .none,
createSut: makeOperator
)
@@ -344,6 +362,38 @@ final class ReduceTests: XCTestCase {
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
}
static func testUpstreamFinishesImmediatelyWithoutDemand<Operator: Publisher>(
expectedSubscription: StringSubscription,
_ makeOperator: (CustomPublisherBase<Int, Error>) -> Operator
) where Operator.Output: Equatable, Operator.Failure == Error {
let helper = OperatorTestHelper(
publisherType: CustomPublisherBase<Int, Error>.self,
initialDemand: nil,
receiveValueDemand: .none,
createSut: makeOperator
)
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription)])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
helper.publisher.send(completion: .finished)
let expectedHistory: [TrackingSubscriberBase<Operator.Output, Error>.Event] =
[.subscription(expectedSubscription)]
XCTAssertEqual(helper.tracking.history, expectedHistory)
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
helper.publisher.send(completion: .failure(TestingError.oops))
XCTAssertEqual(helper.tracking.history, expectedHistory)
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(helper.publisher.send(73), .none)
XCTAssertEqual(helper.tracking.history, expectedHistory)
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
}
static func testCancelAlreadyCancelled<Operator: Publisher>(
_ makeOperator: (CustomPublisherBase<Int, Error>) -> Operator
) throws where Operator.Output: Equatable, Operator.Failure == Error {
@@ -137,7 +137,7 @@ final class RemoveDuplicatesTests: XCTestCase {
func testRemoveDuplicatesCancelBeforeSubscription() {
testCancelBeforeSubscription(inputType: Int.self,
shouldCrash: false,
expected: .history([.cancelled]),
{ $0.removeDuplicates(by: shouldNotBeCalled()) })
}
@@ -324,7 +324,7 @@ final class RemoveDuplicatesTests: XCTestCase {
func testTryRemoveDuplicatesCancelBeforeSubscription() {
testCancelBeforeSubscription(inputType: Int.self,
shouldCrash: false,
expected: .history([.cancelled]),
{ $0.tryRemoveDuplicates(by: shouldNotBeCalled()) })
}
@@ -326,9 +326,11 @@ final class ReplaceEmptyTests: XCTestCase {
}
func testReplaceEmptyCancelBeforeSubscription() {
testCancelBeforeSubscription(inputType: Int.self, shouldCrash: false) {
$0.replaceEmpty(with: 1337)
}
testCancelBeforeSubscription(
inputType: Int.self,
expected: .history([.requested(.unlimited)]),
{ $0.replaceEmpty(with: 1337) }
)
}
func testReplaceEmptyLifecycle() throws {
@@ -246,7 +246,7 @@ final class ReplaceErrorTests: XCTestCase {
func testReplaceErrorCancelBeforeSubscription() {
testCancelBeforeSubscription(inputType: Int.self,
shouldCrash: false,
expected: .history([]),
{ $0.replaceError(with: 0) })
}
}
@@ -0,0 +1,411 @@
//
// RetryTests.swift
//
//
// Created by Sergej Jaskiewicz on 11.07.2020.
//
import XCTest
#if OPENCOMBINE_COMPATIBILITY_TEST
import Combine
#else
import OpenCombine
#endif
@available(macOS 10.15, iOS 13.0, *)
final class RetryTests: XCTestCase {
func testRetry3Times() throws {
let subscription = CustomSubscription()
let publisher = CustomPublisher(subscription: subscription)
let retry = publisher.retry(3)
var downstreamSubscription: Subscription?
let tracking = TrackingSubscriber(
receiveSubscription: {
downstreamSubscription = $0
$0.request(.max(5))
}
)
var upstreamSubscribeCounter = 0
publisher.didSubscribe = { _, _ in
if upstreamSubscribeCounter == 0 {
XCTAssertEqual(tracking.history, [.subscription("Retry")])
}
upstreamSubscribeCounter += 1
}
retry.subscribe(tracking)
XCTAssertEqual(upstreamSubscribeCounter, 1)
XCTAssertEqual(publisher.send(1), .none)
publisher.send(completion: .failure("oops1"))
XCTAssertEqual(tracking.history, [.subscription("Retry"),
.value(1)])
XCTAssertEqual(subscription.history, [.requested(.max(5)),
.requested(.max(4))])
XCTAssertEqual(upstreamSubscribeCounter, 2)
XCTAssertEqual(publisher.send(2), .none)
publisher.send(completion: .failure("oops2"))
XCTAssertEqual(tracking.history, [.subscription("Retry"),
.value(1),
.value(2)])
XCTAssertEqual(subscription.history, [.requested(.max(5)),
.requested(.max(4)),
.requested(.max(3))])
XCTAssertEqual(upstreamSubscribeCounter, 3)
XCTAssertEqual(publisher.send(3), .none)
publisher.send(completion: .failure("oops3"))
XCTAssertEqual(tracking.history, [.subscription("Retry"),
.value(1),
.value(2),
.value(3)])
XCTAssertEqual(subscription.history, [.requested(.max(5)),
.requested(.max(4)),
.requested(.max(3)),
.requested(.max(2))])
XCTAssertEqual(upstreamSubscribeCounter, 4)
XCTAssertEqual(publisher.send(4), .none)
publisher.send(completion: .failure("oops4"))
XCTAssertEqual(tracking.history, [.subscription("Retry"),
.value(1),
.value(2),
.value(3),
.value(4),
.completion(.failure("oops4"))])
XCTAssertEqual(subscription.history, [.requested(.max(5)),
.requested(.max(4)),
.requested(.max(3)),
.requested(.max(2))])
XCTAssertEqual(upstreamSubscribeCounter, 4)
XCTAssertEqual(publisher.send(5), .none)
publisher.send(completion: .failure("oops5"))
XCTAssertEqual(tracking.history, [.subscription("Retry"),
.value(1),
.value(2),
.value(3),
.value(4),
.completion(.failure("oops4"))])
XCTAssertEqual(subscription.history, [.requested(.max(5)),
.requested(.max(4)),
.requested(.max(3)),
.requested(.max(2))])
XCTAssertEqual(upstreamSubscribeCounter, 4)
try XCTUnwrap(downstreamSubscription).request(.max(112))
XCTAssertEqual(subscription.history, [.requested(.max(5)),
.requested(.max(4)),
.requested(.max(3)),
.requested(.max(2))])
}
func testRetry0Times() {
let helper = OperatorTestHelper(
publisherType: CustomPublisher.self,
initialDemand: .max(1),
receiveValueDemand: .none,
createSut: { $0.retry(0) }
)
XCTAssertEqual(helper.publisher.send(1), .none)
helper.publisher.send(completion: .failure(.oops))
XCTAssertEqual(helper.tracking.history, [.subscription("Retry"),
.value(1),
.completion(.failure(.oops))])
XCTAssertEqual(helper.subscription.history, [.requested(.max(1))])
XCTAssertEqual(helper.publisher.send(2), .none)
helper.publisher.send(completion: .finished)
XCTAssertEqual(helper.tracking.history, [.subscription("Retry"),
.value(1),
.completion(.failure(.oops))])
XCTAssertEqual(helper.subscription.history, [.requested(.max(1))])
}
func testRetryForever() {
testRetryForever(attempts: nil)
}
func testRetryNegativeAmountOfTimes() {
testRetryForever(attempts: -1)
}
func testFinishSuccessfullyFirstTime() {
let helper = OperatorTestHelper(
publisherType: CustomPublisher.self,
initialDemand: .max(2),
receiveValueDemand: .none,
createSut: { $0.retry(3) }
)
XCTAssertEqual(helper.publisher.send(1), .none)
XCTAssertEqual(helper.publisher.send(2), .none)
helper.publisher.send(completion: .finished)
helper.publisher.send(completion: .failure(.oops))
XCTAssertEqual(helper.tracking.history, [.subscription("Retry"),
.value(1),
.value(2),
.completion(.finished)])
XCTAssertEqual(helper.subscription.history, [.requested(.max(2))])
}
func testFinishSuccessfullyAfterRetry() {
let helper = OperatorTestHelper(
publisherType: CustomPublisher.self,
initialDemand: .max(4),
receiveValueDemand: .none,
createSut: { $0.retry(3) }
)
XCTAssertEqual(helper.publisher.send(1), .none)
XCTAssertEqual(helper.publisher.send(2), .none)
helper.publisher.send(completion: .failure(.oops))
XCTAssertEqual(helper.publisher.send(3), .none)
XCTAssertEqual(helper.publisher.send(4), .none)
helper.publisher.send(completion: .finished)
XCTAssertEqual(helper.tracking.history, [.subscription("Retry"),
.value(1),
.value(2),
.value(3),
.value(4),
.completion(.finished)])
XCTAssertEqual(helper.subscription.history, [.requested(.max(4)),
.requested(.max(2))])
}
func testCancelAlreadyCancelled() throws {
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
initialDemand: nil,
receiveValueDemand: .none,
createSut: { $0.retry(3) })
XCTAssertEqual(helper.subscription.history, [])
XCTAssertEqual(helper.tracking.history, [.subscription("Retry")])
try XCTUnwrap(helper.downstreamSubscription).cancel()
try XCTUnwrap(helper.downstreamSubscription).request(.max(3))
try XCTUnwrap(helper.downstreamSubscription).cancel()
XCTAssertEqual(helper.publisher.send(42), .none)
XCTAssertEqual(helper.subscription.history, [.cancelled])
XCTAssertEqual(helper.tracking.history, [.subscription("Retry")])
}
func testPreservesDemand() {
let publisher = CustomPublisher(subscription: nil)
let retry = publisher.retry(5)
let tracking = TrackingSubscriber(receiveSubscription: { $0.request(.max(3)) },
receiveValue: { .max($0) })
retry.subscribe(tracking)
XCTAssertEqual(tracking.history, [])
let subscription = CustomSubscription()
publisher.send(subscription: subscription)
XCTAssertEqual(subscription.history, [.requested(.max(3))])
XCTAssertEqual(publisher.send(5), .none)
XCTAssertEqual(tracking.history, [.subscription("Retry"),
.value(5)])
XCTAssertEqual(subscription.history, [.requested(.max(3)),
.requested(.max(5))])
}
func testCrashesOnUnwantedValue() {
let helper = OperatorTestHelper(
publisherType: CustomPublisher.self,
initialDemand: nil,
receiveValueDemand: .none,
createSut: { $0.retry(2) }
)
assertCrashes {
_ = helper.publisher.send(-1)
}
}
func testSubscriptionRecursion() {
let subscription = CustomSubscription()
let publisher = CustomPublisher(subscription: subscription)
let retry = publisher.retry(5)
let tracking = TrackingSubscriber(receiveSubscription: { $0.request(.max(1)) })
var upstreamSubscribeCounter = 0
publisher.didSubscribe = { _, _ in
if upstreamSubscribeCounter > 0 && upstreamSubscribeCounter < 10 {
publisher.send(completion: .failure(.oops))
}
upstreamSubscribeCounter += 1
}
retry.subscribe(tracking)
XCTAssertEqual(tracking.history, [.subscription("Retry")])
XCTAssertEqual(subscription.history, [.requested(.max(1))])
XCTAssertEqual(upstreamSubscribeCounter, 1)
publisher.send(completion: .failure(.oops))
XCTAssertEqual(tracking.history, [.subscription("Retry"),
.completion(.failure(.oops))])
XCTAssertEqual(subscription.history,
Array(repeating: .requested(.max(1)), count: 6))
XCTAssertEqual(upstreamSubscribeCounter, 6)
}
func testRecurseAndFinish() {
let subscription = CustomSubscription()
let publisher = CustomPublisher(subscription: subscription)
let retry = publisher.retry(5)
let tracking = TrackingSubscriber(receiveSubscription: { $0.request(.max(1)) })
var upstreamSubscribeCounter = 0
publisher.didSubscribe = { _, _ in
if upstreamSubscribeCounter > 0 {
if upstreamSubscribeCounter < 5 {
publisher.send(completion: .failure(.oops))
} else {
publisher.send(completion: .finished)
}
}
upstreamSubscribeCounter += 1
}
retry.subscribe(tracking)
XCTAssertEqual(tracking.history, [.subscription("Retry")])
XCTAssertEqual(subscription.history, [.requested(.max(1))])
XCTAssertEqual(upstreamSubscribeCounter, 1)
publisher.send(completion: .failure(.oops))
XCTAssertEqual(tracking.history, [.subscription("Retry"),
.completion(.finished)])
XCTAssertEqual(subscription.history,
Array(repeating: .requested(.max(1)), count: 6))
XCTAssertEqual(upstreamSubscribeCounter, 6)
}
func testRecurseAndReceiveValue() {
let subscription = CustomSubscription()
let publisher = CustomPublisher(subscription: subscription)
let retry = publisher.retry(1)
let tracking = TrackingSubscriber(receiveSubscription: { $0.request(.max(3)) },
receiveValue: { _ in .max(2) })
var upstreamSubscribeCounter = 0
publisher.willSubscribe = { _, _ in
if upstreamSubscribeCounter > 0 {
XCTAssertEqual(publisher.send(1), .none)
}
upstreamSubscribeCounter += 1
}
retry.subscribe(tracking)
XCTAssertEqual(tracking.history, [.subscription("Retry")])
XCTAssertEqual(subscription.history, [.requested(.max(3))])
XCTAssertEqual(upstreamSubscribeCounter, 1)
publisher.send(completion: .failure(.oops))
XCTAssertEqual(tracking.history, [.subscription("Retry"),
.value(1)])
XCTAssertEqual(subscription.history, [.requested(.max(3)),
.requested(.max(4))])
XCTAssertEqual(upstreamSubscribeCounter, 2)
}
func testRetryReceiveValueBeforeSubscription() {
testReceiveValueBeforeSubscription(
value: 31,
expected: .crash,
{ $0.retry(3) }
)
}
func testRetryReceiveCompletionBeforeSubscription() {
testReceiveCompletionBeforeSubscription(
inputType: Int.self,
expected: .history([.completion(.finished)]),
{ $0.retry(3) }
)
}
func testRetryRequestBeforeSubscription() {
testRequestBeforeSubscription(inputType: Int.self,
shouldCrash: false,
{ $0.retry(3) })
}
func testRetryCancelBeforeSubscription() {
testCancelBeforeSubscription(
inputType: Int.self,
expected: .history([.cancelled]),
{ $0.retry(3) }
)
}
func testRetryReceiveSubscriptionTwice() throws {
try testReceiveSubscriptionTwice { $0.retry(3) }
}
func testRetryLifecycle() throws {
try testLifecycle(sendValue: 31,
cancellingSubscriptionReleasesSubscriber: true,
{ $0.retry(3) })
}
func testRetryReflection() throws {
try testReflection(
parentInput: Int.self,
parentFailure: TestingError.self,
description: "Retry",
customMirror: childrenIsEmpty,
playgroundDescription: "Retry",
{ $0.retry(3) }
)
}
// MARK: - Private
private func testRetryForever(attempts: Int?) {
let subscription = CustomSubscription()
let publisher = CustomPublisher(subscription: subscription)
let retry = Publishers.Retry(upstream: publisher, retries: attempts)
let tracking = TrackingSubscriber(receiveSubscription: { $0.request(.max(1)) })
var upstreamSubscribeCounter = 0
publisher.didSubscribe = { _, _ in
upstreamSubscribeCounter += 1
}
retry.subscribe(tracking)
XCTAssertEqual(upstreamSubscribeCounter, 1)
for _ in 0 ..< 10000 {
publisher.send(completion: .failure(.oops))
}
XCTAssertEqual(publisher.send(0), .none)
XCTAssertEqual(tracking.history, [.subscription("Retry"),
.value(0)])
XCTAssertEqual(subscription.history,
Array(repeating: .requested(.max(1)), count: 10001))
XCTAssertEqual(upstreamSubscribeCounter, 10001)
}
}
@@ -336,7 +336,7 @@ final class ScanTests: XCTestCase {
func testTryScanCancelBeforeSubscription() {
testCancelBeforeSubscription(inputType: Int.self,
shouldCrash: false,
expected: .history([]),
{ $0.tryScan(0, shouldNotBeCalled()) })
}
@@ -311,7 +311,7 @@ final class SubscribeOnTests: XCTestCase {
}
func testSubscribeOnCancelBeforeSubscription() {
testCancelBeforeSubscription(inputType: Int.self, shouldCrash: false) {
testCancelBeforeSubscription(inputType: Int.self, expected: .history([])) {
$0.subscribe(on: ImmediateScheduler.shared)
}
}
@@ -736,6 +736,7 @@ final class SwitchToLatestTests: XCTestCase {
XCTAssertEqual(nestedSubscription.history, [.requested(.max(1))])
}
@available(macOS 11.0, iOS 14.0, *)
func testOverloadWhenUpstreamNeverFailsButChildrenCanFail() {
let helper = OperatorTestHelper(
publisherType: CustomPublisherBase<CustomPublisher, Never>.self,
@@ -747,6 +748,7 @@ final class SwitchToLatestTests: XCTestCase {
XCTAssertEqual(helper.sut.upstream.upstream, helper.publisher)
}
@available(macOS 11.0, iOS 14.0, *)
func testOverloadWhenUpstreamCanFailButChildrenNeverFail() {
let helper = OperatorTestHelper(
publisherType: CustomPublisherBase<CustomPublisherBase<Int, Never>,
@@ -0,0 +1,844 @@
//
// ThrottleTests.swift
//
//
// Created by Stuart Austin on 14/11/2020.
//
import XCTest
#if OPENCOMBINE_COMPATIBILITY_TEST
import Combine
#else
import OpenCombine
#endif
@available(macOS 10.15, iOS 13.0, *)
final class ThrottleTests: XCTestCase {
func testBasicBehavior() {
let scheduler = VirtualTimeScheduler()
let extractedExpr = OperatorTestHelper(publisherType: CustomPublisher.self,
initialDemand: .max(100),
receiveValueDemand: .max(12)) {
$0.throttle(for: .seconds(1337), scheduler: scheduler, latest: true)
}
let helper = extractedExpr
XCTAssertNotNil(helper.publisher.subscriber,
"Subscription must be performed synchronously")
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle")])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(scheduler.history, [.now, // Subscriber created
.now]) // Subscription received by Subscriber
// Send an initial value to the subject. This should be scheduled immediately
XCTAssertEqual(helper.publisher.send(1), .none)
XCTAssertEqual(scheduler.history, [.now,
.now,
// Checking the time when the Subscriber
// receives the input "1"
.now,
// Scheduling the output
// of the input immediately as we have not
// output any values
.schedule(options: nil)
])
// Send some more values to the subject. Since we haven't run the scheduled
// output above, these won't create any additional scheduled work
XCTAssertEqual(helper.publisher.send(2), .none)
XCTAssertEqual(helper.publisher.send(3), .none)
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle")])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(scheduler.scheduledDates, [.nanoseconds(0)])
XCTAssertEqual(scheduler.history, [.now,
.now,
.now,
.schedule(options: nil),
// Checking the time when the Subscriber
// receives the input "2"
.now,
// Checking the time when the Subscriber
// receives the input "3"
.now])
scheduler.executeScheduledActions()
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle"),
// Expect only "3" to be output
.value(3)])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(scheduler.history, [.now,
.now,
.now,
.schedule(options: nil),
.now,
.now,
.now]) // Log the time of the output
// Send another value to the subject. This should be scheduled after the interval
XCTAssertEqual(helper.publisher.send(4), .none)
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle"),
.value(3)])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(scheduler.scheduledDates, [.seconds(1337)])
XCTAssertEqual(scheduler.history, [.now,
.now,
.now,
.schedule(options: nil),
.now,
.now,
.now,
// Checking the time when the Subscriber
// receives the input "4"
.now,
// When scheduling the output, it uses
// the minimum tolerance
.minimumTolerance,
// Scheduling of the output
.scheduleAfterDate(.seconds(1337),
tolerance: .nanoseconds(7),
options: nil)])
helper.publisher.send(completion: .failure(.oops))
helper.publisher.send(completion: .finished)
XCTAssertEqual(helper.publisher.send(5), .none)
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle"),
.value(3)])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(scheduler.scheduledDates, [.seconds(1337)])
XCTAssertEqual(scheduler.history, [.now,
.now,
.now,
.schedule(options: nil),
.now,
.now,
.now,
.now,
.minimumTolerance,
.scheduleAfterDate(.seconds(1337),
tolerance: .nanoseconds(7),
options: nil),
.now])
scheduler.executeScheduledActions()
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle"),
.value(3),
.value(4),
.completion(.failure(.oops))])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(scheduler.history, [.now,
.now,
.now,
.schedule(options: nil),
.now,
.now,
.now,
.now,
.minimumTolerance,
.scheduleAfterDate(.seconds(1337),
tolerance: .nanoseconds(7),
options: nil),
.now])
XCTAssertEqual(scheduler.now, .seconds(1337))
}
func testThrottleDemand() {
let scheduler = VirtualTimeScheduler()
let extractedExpr = OperatorTestHelper(publisherType: CustomPublisher.self,
initialDemand: .max(2),
receiveValueDemand: .none) {
$0.throttle(for: .seconds(1337), scheduler: scheduler, latest: false)
}
let helper = extractedExpr
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle")])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(scheduler.history, [.now, // Subscriber created
.now]) // Subscription received by Subscriber
// Send an initial value to the subject. This should be scheduled immediately
XCTAssertEqual(helper.publisher.send(1), .none)
XCTAssertEqual(scheduler.history, [.now,
.now,
// Checking the time when the Subscriber
// receives the input "1"
.now,
// Scheduling the output of the input
// immediately as we have not output any values
.schedule(options: nil)])
// Send some more values to the subject.
// Since we haven't run the scheduled output above, these won't create
// any additional scheduled work
XCTAssertEqual(helper.publisher.send(5), .none)
XCTAssertEqual(helper.publisher.send(6), .none)
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle")])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(scheduler.scheduledDates, [.nanoseconds(0)])
XCTAssertEqual(scheduler.history, [.now,
.now,
.now,
.schedule(options: nil),
// Checking the time when the Subscriber
// receives the input "5"
.now,
// Checking the time when the Subscriber
// receives the input "6"
.now])
scheduler.executeScheduledActions()
// Send a second value to the subject. This should be scheduled after the interval
XCTAssertEqual(helper.publisher.send(2), .none)
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle"),
.value(1)])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(scheduler.scheduledDates, [.seconds(1337)])
XCTAssertEqual(scheduler.history, [.now,
.now,
.now,
.schedule(options: nil),
.now,
.now,
.now,
// Checking the time when the Subscriber
// receives the input "2"
.now,
// When scheduling the output, it uses
// the minimum tolerance
.minimumTolerance,
// Scheduling of the output
.scheduleAfterDate(.seconds(1337),
tolerance: .nanoseconds(7),
options: nil)])
scheduler.executeScheduledActions()
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle"),
.value(1),
.value(2)])
// Send a third value to the subject.
// This should not be output at all due to the demand
XCTAssertEqual(helper.publisher.send(3), .none)
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle"),
.value(1),
.value(2)])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(scheduler.scheduledDates, [])
XCTAssertEqual(scheduler.history, [.now,
.now,
.now,
.schedule(options: nil),
.now,
.now,
.now,
// Checking the time when the Subscriber
// receives the input "4"
.now,
// When scheduling the output, it uses
// the minimum tolerance
.minimumTolerance,
// Scheduling of the output
.scheduleAfterDate(.seconds(1337),
tolerance: .nanoseconds(7),
options: nil),
.now,
.now])
scheduler.executeScheduledActions()
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle"),
.value(1),
.value(2)])
}
func testThrottleGap() {
let scheduler = VirtualTimeScheduler()
let extractedExpr = OperatorTestHelper(publisherType: CustomPublisher.self,
initialDemand: .unlimited,
receiveValueDemand: .none) {
$0.throttle(for: .seconds(60), scheduler: scheduler, latest: false)
}
let helper = extractedExpr
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle")])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(scheduler.history, [.now,
.now])
XCTAssertEqual(helper.publisher.send(0), .none)
XCTAssertEqual(scheduler.history, [.now,
.now,
.now,
.schedule(options: nil)])
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle")])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(scheduler.scheduledDates, [.nanoseconds(0)])
XCTAssertEqual(scheduler.history, [.now,
.now,
.now,
.schedule(options: nil)])
scheduler.executeScheduledActions()
XCTAssertEqual(scheduler.history, [.now,
.now,
.now,
.schedule(options: nil),
.now])
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle"), .value(0)])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(scheduler.scheduledDates, [])
var future = scheduler.now + .seconds(45)
XCTAssertEqual(scheduler.history, [.now,
.now,
.now,
.schedule(options: nil),
.now,
.now])
// change the current time to be 45 seconds into the future
scheduler.rewind(to: future)
XCTAssertEqual(helper.publisher.send(1), .none)
XCTAssertEqual(scheduler.history, [.now,
.now,
.now,
.schedule(options: nil),
.now,
.now,
.now,
.minimumTolerance,
.scheduleAfterDate(.seconds(60),
tolerance: .nanoseconds(7),
options: nil)])
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle"), .value(0)])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
// next value should be emitted 60 seconds from the start of time
XCTAssertEqual(scheduler.scheduledDates, [.seconds(60)])
scheduler.executeScheduledActions()
XCTAssertEqual(scheduler.history, [.now,
.now,
.now,
.schedule(options: nil),
.now,
.now,
.now,
.minimumTolerance,
.scheduleAfterDate(.seconds(60),
tolerance: .nanoseconds(7),
options: nil),
.now])
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle"),
.value(0),
.value(1)])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(scheduler.scheduledDates, [])
future = scheduler.now + .seconds(61)
XCTAssertEqual(scheduler.history, [.now,
.now,
.now,
.schedule(options: nil),
.now,
.now,
.now,
.minimumTolerance,
.scheduleAfterDate(.seconds(60),
tolerance: .nanoseconds(7),
options: nil),
.now,
.now])
// change the current time to be 61 seconds into the future
scheduler.rewind(to: future)
XCTAssertEqual(helper.publisher.send(2), .none)
XCTAssertEqual(scheduler.history, [.now,
.now,
.now,
.schedule(options: nil),
.now,
.now,
.now,
.minimumTolerance,
.scheduleAfterDate(.seconds(60),
tolerance: .nanoseconds(7),
options: nil),
.now,
.now,
.now,
// next value should be scheduled immediately
// as the interval has passed
.schedule(options: nil)])
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle"),
.value(0),
.value(1)])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
// next value should be emitted 121 seconds from the start of time
XCTAssertEqual(scheduler.scheduledDates, [.seconds(121)])
scheduler.executeScheduledActions()
XCTAssertEqual(scheduler.history, [.now,
.now,
.now,
.schedule(options: nil),
.now,
.now,
.now,
.minimumTolerance,
.scheduleAfterDate(.seconds(60),
tolerance: .nanoseconds(7),
options: nil),
.now,
.now,
.now,
.schedule(options: nil),
.now])
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle"),
.value(0),
.value(1),
.value(2)])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(scheduler.scheduledDates, [])
}
func testRequest() throws {
let scheduler = VirtualTimeScheduler()
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
initialDemand: nil,
receiveValueDemand: .none) {
$0.throttle(for: .seconds(10), scheduler: scheduler, latest: true)
}
scheduler.executeScheduledActions()
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle")])
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("Throttle")])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
scheduler.executeScheduledActions()
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle"),
.value(2000)])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
}
func testCancelAlreadyCancelled() throws {
let scheduler = VirtualTimeScheduler()
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
initialDemand: .unlimited,
receiveValueDemand: .none) {
$0.throttle(for: .seconds(10), scheduler: scheduler, latest: true)
}
scheduler.executeScheduledActions()
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle")])
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("Throttle")])
XCTAssertEqual(scheduler.history, [.now, .now])
XCTAssertEqual(helper.publisher.send(0), .none)
helper.publisher.send(completion: .finished)
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited), .cancelled])
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle")])
XCTAssertEqual(scheduler.history, [.now, .now])
}
func testNoDemandReceivesNoValues() throws {
let subscription = CustomSubscription()
let publisher = CustomPublisher(subscription: subscription)
let tracking = TrackingSubscriber(
receiveValue: { _ in
XCTFail("Unexpected value received")
return .none
}
)
let throttle = publisher.throttle(for: .milliseconds(1),
scheduler: ImmediateScheduler.shared,
latest: true)
throttle.subscribe(tracking)
XCTAssertEqual(tracking.history, [.subscription("Throttle")])
XCTAssertEqual(subscription.history, [.requested(.unlimited)])
XCTAssertEqual(publisher.send(1), .none)
XCTAssertEqual(tracking.history, [.subscription("Throttle")])
XCTAssertEqual(subscription.history, [.requested(.unlimited)])
XCTAssertEqual(tracking.history, [.subscription("Throttle")])
XCTAssertEqual(subscription.history, [.requested(.unlimited)])
}
func testCancelWhileReceivingInput() throws {
let scheduler = VirtualTimeScheduler()
let subscription = CustomSubscription()
let publisher = CustomPublisher(subscription: subscription)
var downstreamSubscription: Subscription?
let tracking = TrackingSubscriber(
receiveSubscription: {
downstreamSubscription = $0
$0.request(.unlimited)
},
receiveValue: { _ in
XCTAssertNotNil(downstreamSubscription)
downstreamSubscription?.cancel()
return .max(42)
}
)
let throttle = publisher.throttle(for: .seconds(60),
scheduler: scheduler,
latest: true)
throttle.subscribe(tracking)
XCTAssertEqual(tracking.history, [.subscription("Throttle")])
XCTAssertEqual(subscription.history, [.requested(.unlimited)])
XCTAssertEqual(scheduler.history, [.now, .now])
XCTAssertEqual(publisher.send(1), .none)
XCTAssertEqual(tracking.history, [.subscription("Throttle")])
XCTAssertEqual(subscription.history, [.requested(.unlimited)])
XCTAssertEqual(scheduler.history, [.now, .now, .now, .schedule(options: nil)])
scheduler.executeScheduledActions()
XCTAssertEqual(tracking.history, [.subscription("Throttle"), .value(1)])
XCTAssertEqual(subscription.history, [.requested(.unlimited), .cancelled])
XCTAssertEqual(scheduler.history, [.now,
.now,
.now,
.schedule(options: nil),
.now])
}
func testCancelWhilstScheduledOutput() {
let scheduler = VirtualTimeScheduler()
let subscription = CustomSubscription()
let publisher = CustomPublisher(subscription: subscription)
var downstreamSubscription: Subscription?
let tracking = TrackingSubscriber(
receiveSubscription: {
downstreamSubscription = $0
$0.request(.unlimited)
},
receiveValue: { _ in
XCTAssertNotNil(downstreamSubscription)
return .max(42)
}
)
let throttle = publisher.throttle(for: .seconds(60),
scheduler: scheduler,
latest: true)
throttle.subscribe(tracking)
XCTAssertEqual(tracking.history, [.subscription("Throttle")])
XCTAssertEqual(subscription.history, [.requested(.unlimited)])
XCTAssertEqual(scheduler.history, [.now, .now])
XCTAssertEqual(publisher.send(1), .none)
XCTAssertEqual(tracking.history, [.subscription("Throttle")])
XCTAssertEqual(subscription.history, [.requested(.unlimited)])
XCTAssertEqual(scheduler.history, [.now, .now, .now, .schedule(options: nil)])
tracking.cancel()
scheduler.executeScheduledActions()
XCTAssertEqual(tracking.history, [])
XCTAssertEqual(subscription.history, [.requested(.unlimited), .cancelled])
XCTAssertEqual(scheduler.history, [.now,
.now,
.now,
.schedule(options: nil)])
}
func testReceiveCompletionImmediatelyAfterSubscription() {
let scheduler = VirtualTimeScheduler()
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
initialDemand: .unlimited,
receiveValueDemand: .none) {
$0.throttle(for: .seconds(60), scheduler: scheduler, latest: true)
}
helper.publisher.send(completion: .failure(.oops))
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle")])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(scheduler.history, [.now, .now, .now, .schedule(options: nil)])
scheduler.executeScheduledActions()
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle"),
.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.throttle(for: .seconds(60), scheduler: scheduler, latest: true)
}
XCTAssertEqual(helper.publisher.send(-1), .none)
scheduler.executeScheduledActions()
XCTAssertEqual(helper.publisher.send(1000), .none)
helper.publisher.send(completion: .finished)
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle"),
.value(-1)])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(scheduler.history, [.now,
.now,
.now,
.schedule(options: nil),
.now,
.now,
.minimumTolerance,
.scheduleAfterDate(.seconds(60),
tolerance: .nanoseconds(7),
options: nil),
.now])
scheduler.executeScheduledActions()
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle"),
.value(-1),
.value(1000),
.completion(.finished)])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
}
func testReceiveInputRecursively() {
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
initialDemand: .unlimited,
receiveValueDemand: .max(418)) {
$0.throttle(for: .seconds(60),
scheduler: ImmediateScheduler.shared,
latest: true)
}
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("Throttle"),
.value(0),
.value(-1),
.value(-1),
.value(-1),
.value(-1),
.value(-1)])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
}
func testReceiveCompletionRecursively() {
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
initialDemand: .unlimited,
receiveValueDemand: .max(418)) {
$0.throttle(for: .seconds(60),
scheduler: ImmediateScheduler.shared,
latest: true)
}
helper.tracking.onFinish = {
helper.publisher.send(completion: .finished)
}
helper.publisher.send(completion: .finished)
}
func testWeakCaptureWhenSchedulingValue() {
let scheduler = VirtualTimeScheduler()
var value: Int?
var subscriberReleased = false
do {
let publisher = CustomPublisher(subscription: CustomSubscription())
let throttle = publisher.throttle(for: .seconds(60),
scheduler: scheduler,
latest: true)
let tracking =
TrackingSubscriber(receiveSubscription: { $0.request(.unlimited) },
receiveValue: { value = $0; return .none },
onDeinit: { subscriberReleased = true })
throttle.subscribe(tracking)
scheduler.executeScheduledActions()
XCTAssertEqual(tracking.history, [.subscription("Throttle")])
XCTAssertEqual(publisher.send(42), .none)
XCTAssertEqual(tracking.history, [.subscription("Throttle")])
XCTAssertEqual(scheduler.history, [.now, .now, .now, .schedule(options: nil)])
tracking.cancel()
publisher.cancel()
}
XCTAssertTrue(subscriberReleased)
XCTAssertNil(value)
}
func testWeakCaptureWhenSchedulingCompletion() {
let scheduler = VirtualTimeScheduler()
var completion: Subscribers.Completion<TestingError>?
var subscriberReleased = false
do {
let publisher = CustomPublisher(subscription: CustomSubscription())
let throttle = publisher.throttle(for: .seconds(60),
scheduler: scheduler,
latest: true)
let tracking = TrackingSubscriber(receiveCompletion: { completion = $0 },
onDeinit: { subscriberReleased = true })
throttle.subscribe(tracking)
scheduler.executeScheduledActions()
XCTAssertEqual(tracking.history, [.subscription("Throttle")])
publisher.send(completion: .finished)
XCTAssertEqual(tracking.history, [.subscription("Throttle")])
XCTAssertEqual(scheduler.history, [.now, .now, .now, .schedule(options: nil)])
tracking.cancel()
publisher.cancel()
}
XCTAssertTrue(subscriberReleased)
XCTAssertNil(completion)
scheduler.executeScheduledActions()
XCTAssertNil(completion)
}
func testThrottleReceiveSubscriptionTwice() throws {
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
initialDemand: nil,
receiveValueDemand: .none) {
$0.throttle(for: .seconds(60),
scheduler: ImmediateScheduler.shared,
latest: true)
}
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
let secondSubscription = CustomSubscription()
try XCTUnwrap(helper.publisher.subscriber)
.receive(subscription: secondSubscription)
XCTAssertEqual(secondSubscription.history, [.cancelled])
try XCTUnwrap(helper.publisher.subscriber)
.receive(subscription: helper.subscription)
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
.cancelled])
try XCTUnwrap(helper.downstreamSubscription).cancel()
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
.cancelled,
.cancelled])
}
func testThrottleReceiveValueBeforeSubscription() {
testReceiveValueBeforeSubscription(value: 213,
expected: .history([], demand: .none)) {
$0.throttle(for: .seconds(60),
scheduler: ImmediateScheduler.shared,
latest: true)
}
}
func testThrottleReceiveCompletionBeforeSubscription() {
testReceiveCompletionBeforeSubscription(inputType: Int.self,
expected: .history([])) {
$0.throttle(for: .seconds(60),
scheduler: ImmediateScheduler.shared,
latest: true)
}
}
func testThrottleRequestBeforeSubscription() {
testRequestBeforeSubscription(inputType: Int.self, shouldCrash: false) {
$0.throttle(for: .seconds(60),
scheduler: ImmediateScheduler.shared,
latest: true)
}
}
func testThrottleCancelBeforeSubscription() {
testCancelBeforeSubscription(inputType: Int.self,
expected: .history([.cancelled])) {
$0.throttle(for: .seconds(60),
scheduler: ImmediateScheduler.shared,
latest: true)
}
}
func testThrottleReflection() throws {
try testReflection(parentInput: String.self,
parentFailure: Error.self,
description: "Throttle",
customMirror: childrenIsEmpty,
playgroundDescription: "Throttle",
{ $0.throttle(for: .seconds(60),
scheduler: ImmediateScheduler.shared,
latest: true) })
}
func testThrottleLifecycle() throws {
try testLifecycle(sendValue: 31,
cancellingSubscriptionReleasesSubscriber: true) {
$0.throttle(for: .seconds(60),
scheduler: ImmediateScheduler.shared,
latest: true)
}
}
}
@@ -672,7 +672,7 @@ final class TimeoutTests: XCTestCase {
let scheduler = VirtualTimeScheduler()
testCancelBeforeSubscription(
inputType: Int.self,
shouldCrash: false,
expected: .history([]),
{ $0.timeout(.nanoseconds(13), scheduler: scheduler) }
)
}
@@ -197,6 +197,7 @@ final class SubscribersDemandTests: XCTestCase {
XCTAssertEqual(Subscribers.Demand.unlimited.description, "unlimited")
}
#if !WASI
func testEncodeDecodeJSON() throws {
try testEncodeDecode(
encoder: JSONEncoder(),
@@ -315,6 +316,8 @@ final class SubscribersDemandTests: XCTestCase {
XCTAssertEqual(decodedIllFormedTooBig.value.description, "unlimited")
}
#endif // !WASI
}
@available(macOS 10.15, iOS 13.0, *)
+4
View File
@@ -3,3 +3,7 @@ def suffix_variadic(name, index, arity):
def list_with_suffix_variadic(name, arity):
return [suffix_variadic(name, i, arity) for i in range(arity)]
def indent(input, space_count):
padding = space_count * ' '
return ''.join(padding + line for line in input.splitlines(True))