Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 28993ae57d | |||
| 3d61bf87e7 | |||
| 911a4e1aa3 | |||
| beb38dec0e | |||
| 1fbf688897 | |||
| 5436868053 | |||
| 4977ca158f | |||
| 96214ac5f9 | |||
| 21fda909f5 | |||
| 8438d09b82 | |||
| 30a60b52cc | |||
| a93ed143fb | |||
| e054a884ef |
@@ -114,35 +114,35 @@ jobs:
|
||||
SWIFT_VERSION: "5.3.0"
|
||||
<<: *macOS_tests_steps
|
||||
|
||||
"Execute compatibility tests on iOS 14.1 (Xcode 12.1.0, Swift 5.3.0)":
|
||||
"Execute compatibility tests on iOS 14.2 (Xcode 12.2.0, Swift 5.3.1)":
|
||||
macos:
|
||||
xcode: "12.1.0"
|
||||
xcode: "12.2.0"
|
||||
environment:
|
||||
SWIFT_VERSION: "5.3.0"
|
||||
SWIFT_VERSION: "5.3.1"
|
||||
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.2 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 11,OS=14.2" \
|
||||
-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.2 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 11,OS=14.2" \
|
||||
-derivedDataPath DerivedData \
|
||||
| tee xcodebuild_test-without-building.log \
|
||||
| xcpretty --report junit -o build/reports/results.xml
|
||||
@@ -279,7 +279,7 @@ workflows:
|
||||
- "Execute tests on macOS 10.15.0 (Xcode 12.1.0, Swift 5.3.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.2 (Xcode 12.2.0, Swift 5.3.1)"
|
||||
"OpenCombine: execute tests on iOS":
|
||||
jobs:
|
||||
- "Execute tests on iOS 9.3 (Xcode 10.2.1, Swift 5.0.1)"
|
||||
|
||||
@@ -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
|
||||
|
||||
+299
@@ -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`
|
||||
+1
-1
@@ -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,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,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
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
@@ -2,36 +2,46 @@
|
||||
[](https://circleci.com/gh/OpenCombine/OpenCombine)
|
||||
[](https://codecov.io/gh/OpenCombine/OpenCombine)
|
||||

|
||||

|
||||

|
||||

|
||||
[<img src="https://img.shields.io/badge/slack-OpenCombine-yellow.svg?logo=slack">](https://join.slack.com/t/opencombine/shared_invite/enQtNzE2MjE5NzkxODI0LTYxMjkzNDUxZWViZWI1Njc2YjBhODgxNjRjOTdkZTcxOGU2ZjJjZjYxMGI3NWZkN2RkNGFmZTUzNmU3MGE2ZWM)
|
||||
|
||||
Open-source implementation of Apple's [Combine](https://developer.apple.com/documentation/combine) framework for processing values over time.
|
||||
|
||||
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
|
||||
|
||||
@@ -675,64 +675,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.
|
||||
@@ -1106,19 +1048,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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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
|
||||
/// service’s HTML to the downstream publisher and completes normally. Otherwise,
|
||||
/// the retry operator attempts to reestablish the connection. If after three attempts
|
||||
/// the publisher still can’t 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 }
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -721,6 +723,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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -97,7 +97,7 @@ final class AllSatisfyTests: XCTestCase {
|
||||
|
||||
func testAllSatisfyCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([.requested(.unlimited)]),
|
||||
{ $0.allSatisfy(shouldNotBeCalled()) })
|
||||
}
|
||||
|
||||
@@ -217,7 +217,7 @@ final class AllSatisfyTests: XCTestCase {
|
||||
|
||||
func testTryAllSatisfyCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([.requested(.unlimited)]),
|
||||
{ $0.tryAllSatisfy(shouldNotBeCalled()) })
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,7 +197,7 @@ final class CollectByCountTests: XCTestCase {
|
||||
|
||||
func testCollectByCountCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([]),
|
||||
{ $0.collect(19) })
|
||||
}
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ final class CollectTests: XCTestCase {
|
||||
|
||||
func testCollectCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([.requested(.unlimited)]),
|
||||
{ $0.collect() })
|
||||
}
|
||||
|
||||
|
||||
@@ -336,7 +336,7 @@ final class CompactMapTests: XCTestCase {
|
||||
|
||||
func testTryCompactMapCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([.cancelled]),
|
||||
{ $0.tryCompactMap(shouldNotBeCalled()) })
|
||||
}
|
||||
|
||||
|
||||
@@ -185,19 +185,19 @@ 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()) })
|
||||
}
|
||||
|
||||
@@ -347,11 +347,11 @@ 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()) })
|
||||
}
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ final class ContainsTests: XCTestCase {
|
||||
|
||||
func testContainsCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([.requested(.unlimited)]),
|
||||
{ $0.contains(0) })
|
||||
}
|
||||
|
||||
@@ -199,7 +199,7 @@ final class ContainsTests: XCTestCase {
|
||||
|
||||
func testContainsWhereCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([.requested(.unlimited)]),
|
||||
{ $0.contains(where: shouldNotBeCalled()) })
|
||||
}
|
||||
|
||||
@@ -325,7 +325,7 @@ final class ContainsTests: XCTestCase {
|
||||
|
||||
func testTryContainsWhereCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([.requested(.unlimited)]),
|
||||
{ $0.tryContains(where: shouldNotBeCalled()) })
|
||||
}
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ final class CountTests: XCTestCase {
|
||||
|
||||
func testCountCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([.requested(.unlimited)]),
|
||||
{ $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()) })
|
||||
}
|
||||
|
||||
|
||||
@@ -118,7 +118,7 @@ final class FirstTests: XCTestCase {
|
||||
|
||||
func testFirstCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([.requested(.unlimited)]),
|
||||
{ $0.first() })
|
||||
}
|
||||
|
||||
@@ -271,7 +271,7 @@ final class FirstTests: XCTestCase {
|
||||
|
||||
func testFirstWhereCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([.requested(.unlimited)]),
|
||||
{ $0.first(where: shouldNotBeCalled()) })
|
||||
}
|
||||
|
||||
@@ -439,7 +439,7 @@ final class FirstTests: XCTestCase {
|
||||
|
||||
func testTryFirstWhereCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([.requested(.unlimited)]),
|
||||
{ $0.tryFirst(where: shouldNotBeCalled()) })
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -90,7 +90,7 @@ final class LastTests: XCTestCase {
|
||||
|
||||
func testLastCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([.requested(.unlimited)]),
|
||||
{ $0.last() })
|
||||
}
|
||||
|
||||
@@ -244,7 +244,7 @@ final class LastTests: XCTestCase {
|
||||
|
||||
func testLastWhereCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([.requested(.unlimited)]),
|
||||
{ $0.last(where: shouldNotBeCalled()) })
|
||||
}
|
||||
|
||||
@@ -420,7 +420,7 @@ final class LastTests: XCTestCase {
|
||||
|
||||
func testTryLastWhereCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([.requested(.unlimited)]),
|
||||
{ $0.tryLast(where: shouldNotBeCalled()) })
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -385,10 +385,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) })
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ final class ReduceTests: XCTestCase {
|
||||
|
||||
func testReduceCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([.requested(.unlimited)]),
|
||||
{ $0.reduce(0, shouldNotBeCalled()) })
|
||||
}
|
||||
|
||||
@@ -165,7 +165,7 @@ final class ReduceTests: XCTestCase {
|
||||
|
||||
func testTryReduceCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([.requested(.unlimited)]),
|
||||
{ $0.tryReduce(0, shouldNotBeCalled()) })
|
||||
}
|
||||
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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, *)
|
||||
|
||||
Reference in New Issue
Block a user