13 Commits

Author SHA1 Message Date
Max Desiatov 28993ae57d Add CHANGELOG.md, bump version to 0.12.0 (#202)
* Add CHANGELOG.md, bump version to 0.12.0

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

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

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

* Fixed Danger warning about line length.
2021-01-29 13:42:17 +00:00
Max Desiatov 911a4e1aa3 Add OpenCombineShim product for easier importing (#197) 2021-01-25 17:25:28 +03:00
Yuta Saito beb38dec0e Implementation for ObservableObject with Mirror (#201)
A temporary implementation until we implement the proper type metadata introspection.
2021-01-25 17:24:19 +03:00
Nomo Nomad 1fbf688897 Update README.md (#199) 2020-12-11 16:41:20 +03:00
Sergej Jaskiewicz 5436868053 Fix some lock acquiring in Publishers.FlatMap (#194) 2020-11-08 17:44:33 +03:00
Sergej Jaskiewicz 4977ca158f Update DispatchQueue scheduler to match iOS 14.2 behavior 2020-11-07 17:28:08 +03:00
Sergej Jaskiewicz 96214ac5f9 Run compatibility tests on iOS 14.2 2020-11-07 17:28:08 +03:00
Sergej Jaskiewicz 21fda909f5 Implement Publishers.Retry 2020-11-07 17:28:08 +03:00
Sergej Jaskiewicz 8438d09b82 Increase time intervals in OperationQueue tests
The test is sporadically failing on iOS 9.3.
2020-11-03 17:21:34 +03:00
Sergej Jaskiewicz 30a60b52cc Add missing availability annotations in tests
Fixes #192
2020-11-03 17:21:34 +03:00
Sergej Jaskiewicz a93ed143fb Add more supported platforms to Package.swift 2020-11-03 17:21:34 +03:00
Max Desiatov e054a884ef Add support for SwiftWasm with CI and tests (#191)
WebAssembly support for atomics and multi-threading isn't fully standardized yet, and it not supported in SwiftWasm at the moment. Because of this Dispatch is unavailable, and all Combine-related Foundation stuff is unavailable too. Tests related to this are disabled. Locking functions are replaced with no-op shims.
2020-11-02 22:02:39 +00:00
75 changed files with 1991 additions and 244 deletions
+8 -8
View File
@@ -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)"
+16
View File
@@ -0,0 +1,16 @@
name: SwiftWasm
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
carton_wasmer_test:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- uses: swiftwasm/swiftwasm-action@v5.3
+299
View File
@@ -0,0 +1,299 @@
# 0.12.0 (29 Jan 2021)
This release adds a new `OpenCombineShim` product that will conditionally re-export either
Combine on Apple platforms, or OpenCombine on other platforms. Additionally, `ObservableObject`
protocol is now available and working on all platforms.
A bug with `Timer(timeInterval:repeats:block:)` firing immediately not accounting for the passed
`timeInterval` is fixed.
**Merged pull requests:**
- Fix `Timer(timeInterval:repeats:block:)` not accounting `timeInterval` ([#196](https://github.com/OpenCombine/OpenCombine/pull/196)) via [@grigorye](https://github.com/grigorye)
- Add `OpenCombineShim` product for easier importing ([#197](https://github.com/OpenCombine/OpenCombine/pull/197)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Implementation for `ObservableObject` with `Mirror` ([#201](https://github.com/OpenCombine/OpenCombine/pull/201)) via [@kateinoigakukun](https://github.com/kateinoigakukun)
# 0.11.0 (29 Oct 2020)
This release is compatible with Xcode 12.1.
### Additions
- `Publisher.assigned(to:)` method that accepts a `Published.Publisher`.
- New `Publisher.switchToLatest()` overloads.
- New `Publisher.flatMap(maxPublishers:_:)` overloads.
- `Optional.publisher` property.
- New `_Introspection` protocol that allows to track and explore the subscription graph and data flow.
### Bugfixes
- The project should now compile without warnings.
- The following entities have been updated to match the behavior of the newest Combine version:
- `Subscribers.Assign`
- `Publishers.Breakpoint`
- `Publishers.Buffer`
- `CombineIdentifier`
- `Publishers.CompactMap`
- `Publishers.Concatenate`
- `Publishers.Debounce`
- `Publishers.Delay`
- `DispatchQueue.SchedulerTimeType.Stride`
- `Publishers.Drop`
- `Publishers.Encode`
- `Publishers.Decode`
- `Publishers.Filter`
- `Publishers.HandleEvents`
- `Publishers.IgnoreOutput`
- `Publishers.MeasureInterval`
- `OperationQueue` scheduler
- `Published`
- `Publishers.ReceiveOn`
- `Publishers.ReplaceError`
- `RunLoop scheduler`
- `Publishers.Sequence`
- `Subscribers.Sink`
- `Publishers.SubscribeOn`
- `Publishers.Timeout`
- `Timer` publisher
### Known issues
- The default implementation of the `objectWillChange` requirement of the `ObservableObject` protocol is not available in Swift 5.1 and later.
# 0.10.2 (23 Oct 2020)
### Bugfixes
- Fixed a crash caused by recursive acquisition of a non-recursive lock in SubbjectSubscriber (#186, thanks @stuaustin for the bug report)
### Known issues
- The default implementation of the `objectWillChange` requirement of the `ObservableObject` protocol is not available in Swift 5.1 and later.
# 0.10.1 (4 Oct 2020)
### Bugfixes
- Fixed build errors on Linux with Swift 5.0 and Swift 5.3 toolchains (thanks, @adamleonard and @devmaximilian)
### Known issues
- The default implementation of the `objectWillChange` requirement of the `ObservableObject` protocol is not available in Swift 5.1 and later.
# 0.10.0 (28 Jun 2020)
This release is compatible with Xcode 11.5.
### Additions
- `Timer.publish(every:tolerance:on:in:options:)` (#156, thank you @MaxDesiatov)
- `OperationQueue` scheduler (#165)
- `Publishers.Timeout` (#164)
- `Publishers.Debounce` (#133)
### Bugfixes
- `PassthroughSubject`, `CurrentValueSubject` and `Future` have been rewritten from scratch. They are now faster, more correct and no longer leak subscriptions (#170).
### Known issues
- The default implementation of the `objectWillChange` requirement of the `ObservableObject` protocol is not available in Swift 5.1 and later.
# 0.9.0 (12 Jun 2020)
This release is compatible with Xcode 11.5.
### Additions
- The `Subscribers.Demand` struct can be nicely formatted in LLDB (#146, thank you @mayoff).
- `Publishers.SwitchToLatest` (#142).
- The `RunLoop` scheduler in `OpenCombineFoundation` (#131).
- `Publishers.Catch` and `Publishers.TryCatch` (#140).
### Bugfixes
- Worked around a [bug in the Swift compiler](https://bugs.swift.org/browse/SR-11680) when building the `COpenCombineHelpers` target (#145, thank you @mayoff).
- Improved documentation.
### Known issues
- The default implementation of the `objectWillChange` requirement of the `ObservableObject` protocol is not available in Swift 5.1 and later.
# 0.8.0 (17 Jan 2020)
This release is compatible with Xcode 11.3.1.
### Additions
- `Publishers.ReplaceEmpty` (#122, thank you @spadafiva)
- `NotificationCenter.Publisher` (#84)
- `URLSession.DataTaskPublisher` (#127)
- `Publishers.DropUntilOutput` (#136)
- `Publishers.CollectByCount` (#137)
- `Publishers.AssertNoFailure` (#138)
- `Publishers.Buffer` (#143)
### Bugfixes
- Fixed integer overflows in `DispatchQueue.SchedulerTimeType.Stride` (#126, #130)
- Fixed the 'default will never be executed' warning on non-Darwin platforms (like Linux) (#129)
### Known issues
- The default implementation of the `objectWillChange` requirement of the `ObservableObject` protocol is not available in Swift 5.1.
# 0.7.0 (10 Dec 2019)
This release is compatible with Xcode 11.2.1.
### Additions
- `Publishers.Delay` (#114)
- `Publishers.ReceiveOn` (#115)
- `Publishers.SubscribeOn` (#116)
- `Publishers.MeasureInterval` (#117)
- `Publishers.Breakpoint` (#118)
- `Publishers.HandleEvents` (#118)
- `Publishers.Concatenate` (#90)
### Known issues
- The default implementation of the `objectWillChange` requirement of the `ObservableObject` protocol is not available in Swift 5.1.
# 0.6.0 (26 Nov 2019)
This release is compatible with Xcode 11.2.1.
### Thread safety
- `Publishers.IgnoreOutput` has been audited for thread safety (#88)
- `Publishers.DropWhile` and `Publishers.TryDropWhile` have been audited for thread safety (#87)
### Additions
- `Publishers.Output` (#91)
- `Record` (#100)
- `Publishers.RemoveDuplicates`, `Publishers.TryRemoveDuplicates` (#89)
- `Publishers.PrefixWhile`, `Publishers.TryPrefixWhile` (#89)
- `Future` (#107, thanks @MaxDesiatov!)
### Bugfixes
- The behavior of the `Publishers.Encode` and `Publishers.Decode` subscriptions is fixed (#112)
- The behavior of the `Publishers.IgnoreOutput` subscription is fixed (#88)
- The behavior of the `Publishers.Print` subscription is fixed (#92)
- The behavior of the `Publishers.ReplaceError` subscription is fixed (#89)
- The behavior of the `Publishers.Filter` and `Publishers.TryFilter` subscriptions is fixed (#89)
- The behavior of the `Publishers.CompactMap` and `Publishers.TryCompactMap` subscriptions is fixed (#89)
- The behavior of the `Publishers.Multicast` subscription is fixed (#110)
- `Publishers.FlatMap` is reimplemented from scratch. Its behavior is fixed in many ways, it now fully matches that of Combine (#89)
- `@Published` property wrapper is fixed! (#112)
- The behavior of `DispatchQueue.SchedulerTimeType` is fixed to match that of the latest SDKs (#96)
- OpenCombine is now usable on 32 bit platforms. Why? Because we can.
### Known issues
- The default implementation of the `objectWillChange` requirement of the `ObservableObject` protocol is not available in Swift 5.1.
# 0.5.0 (17 Oct 2019)
This release is compatible with Xcode 11.1.
### Additions
- `Publishers.MapKeyPath` (#71)
- `Publishers.Reduce` (#76)
- `Publishers.TryReduce` (#76)
- `Publishers.Last` (#76)
- `Publishers.LastWhere` (#76)
- `Publishers.TryLastWhere` (#76)
- `Publishers.AllSatisfy` (#76)
- `Publishers.TryAllSatisfy` (#76)
- `Publishers.Contains` (#76)
- `Publishers.ContainsWhere` (#76)
- `Publishers.TryContainsWhere` (#76)
- `Publishers.Collect` (#76)
- `Publishers.Comparison` (#76)
- `Publishers.Drop` (#70, thank you @5sw!)
- `Publishers.Scan` (#83, thank you @epatey!)
- `Publishers.TryScan` (#83, thank you @epatey!)
### Bugfixes
- `Publishers.Print` doesn't print a redundant whitespace anymore.
### Known issues
- `@Published` property wrapper doesn't work yet
# 0.4.0 (8 Oct 2019)
This release is compatible with Xcode 11.1.
### Thread safety
- `SubjectSubscriber` (which is used when you subscribe a subject to a publisher) has been audited for thread-safety
- `Publishers.Multicast` has been audited for thread safety (#63)
- `Publishers.TryMap` has been audited for thread safety
- `Just` has been audited for thread safety
- `Optional.Publisher` has been audited for thread safety
- `Publishers.Sequence` has been audited for thread safety
- `Publishers.ReplaceError` has been audited for thread safety
- `Subscribers.Assign` has been audited for thread safety
- `Subscribers.Sink` has been audited for thread safety
### Bugfixes
- The semantics of `Publishers.Print`, `Publishers.TryMap` have been fixed
- Fix `iterator.next()` being called twice in `Publishers.Sequence` (#62)
- The default initializer of `CombineIdentifier` (the one that takes no arguments) is now much faster (#66, #69)
- When `Publishers.Sequence` subscription is cancelled while it emits values, the cancellation is respected (#73, thanks @5sw!)
### Additions
- `DispatchQueueScheduler` (#46)
- `Equatable` conformances for `First`, `ReplaceError`
- Added `eraseToAnyPublisher()` method (#59, thanks @evyasafhouzz for reporting!)
- `Publishers.MakeConnectable` (#61)
- `Publishers.Autoconnect` (#60)
- `Publishers.Share` (#60)
### Known issues
- `@Published` property wrapper doesn't work yet
# 0.3.0 (13 Sep 2019)
Among other things this release is compatible with Xcode 11.0 GM seed.
### Bugfixes
- Store newly send value in internal variable inside CurrentValueObject (#39, thanks @FranzBusch!)
### Additions
- `Filter`/`TryFilter` (#22, thanks @spadafiva!)
- `First`/`FirstWhere`/`TryFirstWhere` (#22, thanks again @spadafiva!)
- `CompactMap`/`TryCompacrMap` (#32)
- `IgnoreOutput` (#44, thanks @epatey!)
- `ReplaceError` (#50, thanks @vladiulianbogdan!)
- `FlatMap` (#45, thanks again @epatey!)
### Known issues
- `@Published` property wrapper doesn't work yet
# 0.2.0 (31 Jul 2019)
Updated for the newest Xcode 11.0 beta 5
# 0.1.0 (4 Jul 2019)
The first pre-pre-pre-alpha release is here!
Lots of stuff still unimplemented.
For now we have:
- `Just`
- `Publishers.Decode`
- `Publishers.DropWhile`
- `Publishers.Empty`
- `Publishers.Encode`
- `Publishers.Fail`
- `Publishers.Map`
- `Publishers.Multicast`
- `Publishers.Once`
- `Publishers.Optional`
- `Publishers.Print`
- `Publishers.Sequence`
- `Subscribers.Assign`
- `Subscribers.Completion`
- `Subscribers.Demand`
- `Subscribers.Sink`
- `AnyCancellable`
- `AnyPublisher`
- `AnySubject`
- `AnySubscriber`
- `Cancellable`
- `CombineIdentifier`
- `ConnectablePublisher`
- `CurrentValueSubject`
- `CustomCombineIdentifierConvertible`
- `ImmediateScheduler`
- `PassthroughSubject`
- `Publisher`
- `Result`
- `Scheduler`
- `Subject`
- `Subscriber`
- `Subscription`
+1 -1
View File
@@ -1,6 +1,6 @@
Pod::Spec.new do |spec|
spec.name = "OpenCombine"
spec.version = "0.11.0"
spec.version = "0.12.0"
spec.summary = "Open source implementation of Apple's Combine framework for processing values over time."
spec.description = <<-DESC
+1 -1
View File
@@ -1,6 +1,6 @@
Pod::Spec.new do |spec|
spec.name = "OpenCombineDispatch"
spec.version = "0.11.0"
spec.version = "0.12.0"
spec.summary = "OpenCombine + Dispatch interoperability"
spec.description = <<-DESC
+1 -1
View File
@@ -1,6 +1,6 @@
Pod::Spec.new do |spec|
spec.name = "OpenCombineFoundation"
spec.version = "0.11.0"
spec.version = "0.12.0"
spec.summary = "OpenCombine + OpenCombineFoundation interoperability"
spec.description = <<-DESC
+73 -9
View File
@@ -1,25 +1,89 @@
// swift-tools-version:5.0
// swift-tools-version:5.3
import PackageDescription
// This list should be updated whenever SwiftPM adds support for a new platform.
// See: https://bugs.swift.org/browse/SR-13814
let supportedPlatforms: [Platform] = [
.macOS,
.iOS,
.watchOS,
.tvOS,
.linux,
.android,
// Disable Windows because of https://bugs.swift.org/browse/SR-13817
// .windows,
.wasi,
]
let package = Package(
name: "OpenCombine",
products: [
.library(name: "OpenCombine", targets: ["OpenCombine"]),
.library(name: "OpenCombineDispatch", targets: ["OpenCombineDispatch"]),
.library(name: "OpenCombineFoundation", targets: ["OpenCombineFoundation"]),
.library(name: "OpenCombineShim", targets: ["OpenCombineShim"]),
],
targets: [
.target(name: "COpenCombineHelpers"),
.target(name: "OpenCombine", dependencies: ["COpenCombineHelpers"]),
.target(
name: "OpenCombine",
dependencies: [
.target(name: "COpenCombineHelpers",
condition: .when(platforms: supportedPlatforms.except([.wasi])))
],
exclude: [
"Publishers/Publishers.Encode.swift.gyb",
"Publishers/Publishers.MapKeyPath.swift.gyb",
"Publishers/Publishers.Catch.swift.gyb"
],
swiftSettings: [.define("WASI", .when(platforms: [.wasi]))]
),
.target(name: "OpenCombineDispatch", dependencies: ["OpenCombine"]),
.target(name: "OpenCombineFoundation", dependencies: ["OpenCombine",
"COpenCombineHelpers"]),
.testTarget(name: "OpenCombineTests",
dependencies: ["OpenCombine",
"OpenCombineDispatch",
"OpenCombineFoundation"],
swiftSettings: [.unsafeFlags(["-enable-testing"])])
.target(
name: "OpenCombineFoundation",
dependencies: [
"OpenCombine",
.target(name: "COpenCombineHelpers",
condition: .when(platforms: supportedPlatforms.except([.wasi])))
]
),
.target(
name: "OpenCombineShim",
dependencies: [
"OpenCombine",
.target(name: "OpenCombineDispatch",
condition: .when(platforms: supportedPlatforms.except([.wasi]))),
.target(name: "OpenCombineFoundation",
condition: .when(platforms: supportedPlatforms.except([.wasi])))
]
),
.testTarget(
name: "OpenCombineTests",
dependencies: [
"OpenCombine",
.target(name: "OpenCombineDispatch",
condition: .when(platforms: supportedPlatforms.except([.wasi]))),
.target(name: "OpenCombineFoundation",
condition: .when(platforms: supportedPlatforms.except([.wasi]))),
],
swiftSettings: [
.unsafeFlags(["-enable-testing"]),
.define("WASI", .when(platforms: [.wasi]))
]
)
],
cxxLanguageStandard: .cxx1z
)
// MARK: Helpers
extension Array where Element == Platform {
func except(_ exceptions: [Platform]) -> [Platform] {
// See: https://bugs.swift.org/browse/SR-13813
let exceptionsDescriptions = exceptions.map(String.init(describing:))
return filter { platform in
!exceptionsDescriptions.contains(String(describing: platform))
}
}
}
+34
View File
@@ -0,0 +1,34 @@
// swift-tools-version:5.0
import PackageDescription
let package = Package(
name: "OpenCombine",
products: [
.library(name: "OpenCombine", targets: ["OpenCombine"]),
.library(name: "OpenCombineDispatch", targets: ["OpenCombineDispatch"]),
.library(name: "OpenCombineFoundation", targets: ["OpenCombineFoundation"]),
.library(name: "OpenCombineShim", targets: ["OpenCombineShim"]),
],
targets: [
.target(name: "COpenCombineHelpers"),
.target(name: "OpenCombine", dependencies: ["COpenCombineHelpers"]),
.target(name: "OpenCombineDispatch", dependencies: ["OpenCombine"]),
.target(name: "OpenCombineFoundation", dependencies: ["OpenCombine",
"COpenCombineHelpers"]),
.target(
name: "OpenCombineShim",
dependencies: [
"OpenCombine",
"OpenCombineDispatch",
"OpenCombineFoundation",
]
),
.testTarget(name: "OpenCombineTests",
dependencies: ["OpenCombine",
"OpenCombineDispatch",
"OpenCombineFoundation"],
swiftSettings: [.unsafeFlags(["-enable-testing"])])
],
cxxLanguageStandard: .cxx1z
)
+34
View File
@@ -0,0 +1,34 @@
// swift-tools-version:5.1
import PackageDescription
let package = Package(
name: "OpenCombine",
products: [
.library(name: "OpenCombine", targets: ["OpenCombine"]),
.library(name: "OpenCombineDispatch", targets: ["OpenCombineDispatch"]),
.library(name: "OpenCombineFoundation", targets: ["OpenCombineFoundation"]),
.library(name: "OpenCombineShim", targets: ["OpenCombineShim"]),
],
targets: [
.target(name: "COpenCombineHelpers"),
.target(name: "OpenCombine", dependencies: ["COpenCombineHelpers"]),
.target(name: "OpenCombineDispatch", dependencies: ["OpenCombine"]),
.target(name: "OpenCombineFoundation", dependencies: ["OpenCombine",
"COpenCombineHelpers"]),
.target(
name: "OpenCombineShim",
dependencies: [
"OpenCombine",
"OpenCombineDispatch",
"OpenCombineFoundation",
]
),
.testTarget(name: "OpenCombineTests",
dependencies: ["OpenCombine",
"OpenCombineDispatch",
"OpenCombineFoundation"],
swiftSettings: [.unsafeFlags(["-enable-testing"])])
],
cxxLanguageStandard: .cxx1z
)
+34
View File
@@ -0,0 +1,34 @@
// swift-tools-version:5.2
import PackageDescription
let package = Package(
name: "OpenCombine",
products: [
.library(name: "OpenCombine", targets: ["OpenCombine"]),
.library(name: "OpenCombineDispatch", targets: ["OpenCombineDispatch"]),
.library(name: "OpenCombineFoundation", targets: ["OpenCombineFoundation"]),
.library(name: "OpenCombineShim", targets: ["OpenCombineShim"]),
],
targets: [
.target(name: "COpenCombineHelpers"),
.target(name: "OpenCombine", dependencies: ["COpenCombineHelpers"]),
.target(name: "OpenCombineDispatch", dependencies: ["OpenCombine"]),
.target(name: "OpenCombineFoundation", dependencies: ["OpenCombine",
"COpenCombineHelpers"]),
.target(
name: "OpenCombineShim",
dependencies: [
"OpenCombine",
"OpenCombineDispatch",
"OpenCombineFoundation",
]
),
.testTarget(name: "OpenCombineTests",
dependencies: ["OpenCombine",
"OpenCombineDispatch",
"OpenCombineFoundation"],
swiftSettings: [.unsafeFlags(["-enable-testing"])])
],
cxxLanguageStandard: .cxx1z
)
+22 -12
View File
@@ -2,36 +2,46 @@
[![OpenCombine](https://circleci.com/gh/OpenCombine/OpenCombine.svg?style=svg)](https://circleci.com/gh/OpenCombine/OpenCombine)
[![codecov](https://codecov.io/gh/OpenCombine/OpenCombine/branch/master/graph/badge.svg)](https://codecov.io/gh/OpenCombine/OpenCombine)
![Language](https://img.shields.io/badge/Swift-5.0-orange.svg)
![Platform](https://img.shields.io/badge/platform-Linux%20%7C%20macOS%20%7C%20iOS%20%7C%20watchOS%20%7C%20tvOS-lightgrey.svg)
![Platform](https://img.shields.io/badge/platform-Linux%20%7C%20macOS%20%7C%20iOS%20%7C%20watchOS%20%7C%20tvOS%20%7C%20Wasm-lightgrey.svg)
![Cocoapods](https://img.shields.io/cocoapods/v/OpenCombine?color=blue)
[<img src="https://img.shields.io/badge/slack-OpenCombine-yellow.svg?logo=slack">](https://join.slack.com/t/opencombine/shared_invite/enQtNzE2MjE5NzkxODI0LTYxMjkzNDUxZWViZWI1Njc2YjBhODgxNjRjOTdkZTcxOGU2ZjJjZjYxMGI3NWZkN2RkNGFmZTUzNmU3MGE2ZWM)
Open-source implementation of Apple's [Combine](https://developer.apple.com/documentation/combine) framework for processing values over time.
The main goal of this project is to provide a compatible, reliable and efficient implementation which can be used on Apple's operating systems before macOS 10.15 and iOS 13, as well as Linux and Windows.
The main goal of this project is to provide a compatible, reliable and efficient implementation which can be used on Apple's operating systems before macOS 10.15 and iOS 13, as well as Linux and WebAssembly.
### Installation
`OpenCombine` contains three public targets: `OpenCombine`, `OpenCombineFoundation` and `OpenCombineDispatch` (the fourth one, `COpenCombineHelpers`, is considered private. Don't import it in your projects).
OpenCombine itself does not have any dependencies. Not even Foundation or Dispatch. If you want to use OpenCombine with Dispatch (for example for using `DispatchQueue` as `Scheduler` for operators like `debounce`, `receive(on:)` etc.), you will need to import both `OpenCombine` and `OpenCombineDispatch`. The same applies to Foundation: if you want to use, for instance, `NotificationCenter` or `URLSession` publishers, you'll need to also import `OpenCombineFoundation`
OpenCombine itself does not have any dependencies. Not even Foundation or Dispatch. If you want to use OpenCombine with Dispatch (for example for using `DispatchQueue` as `Scheduler` for operators like `debounce`, `receive(on:)` etc.), you will need to import both `OpenCombine` and `OpenCombineDispatch`. The same applies to Foundation: if you want to use, for instance, `NotificationCenter` or `URLSession` publishers, you'll need to also import `OpenCombineFoundation`.
If you develop code for multiple platforms, you may find it more convenient to import the
`OpenCombineShim` module instead. It conditionally re-exports Combine on Apple platforms (if
available), and all OpenCombine modules on other platforms. You can import `OpenCombineShim` only
when using SwiftPM. It is not currently available for CocoaPods.
##### Swift Package Manager
###### Swift Package
To add `OpenCombine` to your [SPM](https://swift.org/package-manager/) package, add the `OpenCombine` package to the list of package and target dependencies in your `Package.swift` file.
To add `OpenCombine` to your [SwiftPM](https://swift.org/package-manager/) package, add the `OpenCombine` package to the list of package and target dependencies in your `Package.swift` file. `OpenCombineDispatch` and `OpenCombineFoundation` products are currently not supported on WebAssembly. If your project targets WebAssembly exclusively, you should omit them from the list of your dependencies. If it targets multiple platforms including WebAssembly, depend on them only on non-WebAssembly platforms with [conditional target dependencies](https://github.com/apple/swift-evolution/blob/main/proposals/0273-swiftpm-conditional-target-dependencies.md).
```swift
dependencies: [
.package(url: "https://github.com/OpenCombine/OpenCombine.git", from: "0.11.0")
.package(url: "https://github.com/OpenCombine/OpenCombine.git", from: "0.12.0")
],
targets: [
.target(name: "MyAwesomePackage", dependencies: ["OpenCombine",
"OpenCombineDispatch",
"OpenCombineFoundation"])
.target(
name: "MyAwesomePackage",
dependencies: [
"OpenCombine",
.product(name: "OpenCombineFoundation", package: "OpenCombine"),
.product(name: "OpenCombineDispatch", package: "OpenCombine")
]
),
]
```
###### Xcode
`OpenCombine` can also be added as a SPM dependency directly in your Xcode project *(requires Xcode 11 upwards)*.
`OpenCombine` can also be added as a SwiftPM dependency directly in your Xcode project *(requires Xcode 11 upwards)*.
To do so, open Xcode, use **File****Swift Packages****Add Package Dependency…**, enter the [repository URL](https://github.com/OpenCombine/OpenCombine.git), choose the latest available version, and activate the checkboxes:
@@ -44,9 +54,9 @@ To do so, open Xcode, use **File** → **Swift Packages** → **Add Package Depe
To add `OpenCombine` to a project using [CocoaPods](https://cocoapods.org/), add `OpenCombine` and `OpenCombineDispatch` to the list of target dependencies in your `Podfile`.
```ruby
pod 'OpenCombine', '~> 0.11.0'
pod 'OpenCombineDispatch', '~> 0.11.0'
pod 'OpenCombineFoundation', '~> 0.11.0'
pod 'OpenCombine', '~> 0.12.0'
pod 'OpenCombineDispatch', '~> 0.12.0'
pod 'OpenCombineFoundation', '~> 0.12.0'
```
### Contributing
-71
View File
@@ -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
+17
View File
@@ -9,5 +9,22 @@
import COpenCombineHelpers
#endif
#if WASI
internal struct __UnfairLock { // swiftlint:disable:this type_name
internal static func allocate() -> UnfairLock { return .init() }
internal func lock() {}
internal func unlock() {}
internal func assertOwner() {}
internal func deallocate() {}
}
internal struct __UnfairRecursiveLock { // swiftlint:disable:this type_name
internal static func allocate() -> UnfairRecursiveLock { return .init() }
internal func lock() {}
internal func unlock() {}
internal func deallocate() {}
}
#endif // WASI
internal typealias UnfairLock = __UnfairLock
internal typealias UnfairRecursiveLock = __UnfairRecursiveLock
+50 -18
View File
@@ -42,26 +42,58 @@ public protocol ObservableObject: AnyObject {
var objectWillChange: ObjectWillChangePublisher { get }
}
extension ObservableObject where ObjectWillChangePublisher == ObservableObjectPublisher {
// swiftlint:disable let_var_whitespace
#if swift(>=5.1)
/// A publisher that emits before the object has changed.
@available(*, unavailable, message: """
The default implementation of objectWillChange is not available yet. \
It's being worked on in \
https://github.com/broadwaylamb/OpenCombine/pull/97
""")
public var objectWillChange: ObservableObjectPublisher {
fatalError("unimplemented")
}
#else
public var objectWillChange: ObservableObjectPublisher {
return ObservableObjectPublisher()
}
#endif
// swiftlint:enable let_var_whitespace
private protocol _ObservableObjectProperty {
var objectWillChange: ObservableObjectPublisher? { get nonmutating set }
}
#if swift(>=5.1)
extension Published: _ObservableObjectProperty {}
extension ObservableObject where ObjectWillChangePublisher == ObservableObjectPublisher {
/// A publisher that emits before the object has changed.
public var objectWillChange: ObservableObjectPublisher {
var installedPublisher: ObservableObjectPublisher?
var reflection: Mirror? = Mirror(reflecting: self)
while let aClass = reflection {
for (_, property) in aClass.children {
guard let property = property as? _ObservableObjectProperty else {
// Visit other fields until we meet a @Published field
continue
}
// Now we know that the field is @Published.
if let alreadyInstalledPublisher = property.objectWillChange {
installedPublisher = alreadyInstalledPublisher
// Don't visit other fields, as all @Published fields
// already have a publisher installed.
break
}
// Okay, this field doesn't have a publisher installed.
// This means that other fields don't have it either
// (because we install it only once and fields can't be added at runtime).
var lazilyCreatedPublisher: ObjectWillChangePublisher {
if let publisher = installedPublisher {
return publisher
}
let publisher = ObservableObjectPublisher()
installedPublisher = publisher
return publisher
}
property.objectWillChange = lazilyCreatedPublisher
// Continue visiting other fields.
}
reflection = aClass.superclassMirror
}
return installedPublisher ?? ObservableObjectPublisher()
}
}
#endif
/// A publisher that publishes changes from observable objects.
public final class ObservableObjectPublisher: Publisher {
+24 -12
View File
@@ -107,8 +107,16 @@ public struct Published<Value> {
case value(Value)
case publisher(Publisher)
}
@propertyWrapper
private final class Box {
var wrappedValue: Storage
private var storage: Storage
init(wrappedValue: Storage) {
self.wrappedValue = wrappedValue
}
}
@Box private var storage: Storage
internal var objectWillChange: ObservableObjectPublisher? {
get {
@@ -119,8 +127,8 @@ public struct Published<Value> {
return publisher.subject.objectWillChange
}
}
set {
projectedValue.subject.objectWillChange = newValue
nonmutating set {
getPublisher().subject.objectWillChange = newValue
}
}
@@ -145,7 +153,7 @@ public struct Published<Value> {
///
/// - Parameter initialValue: The publisher's initial value.
public init(wrappedValue: Value) {
storage = .value(wrappedValue)
_storage = Box(wrappedValue: .value(wrappedValue))
}
/// The property for which this instance exposes a publisher.
@@ -153,14 +161,7 @@ public struct Published<Value> {
/// The `projectedValue` is the property accessed with the `$` operator.
public var projectedValue: Publisher {
mutating get {
switch storage {
case .value(let value):
let publisher = Publisher(value)
storage = .publisher(publisher)
return publisher
case .publisher(let publisher):
return publisher
}
return getPublisher()
}
set { // swiftlint:disable:this unused_setter_value
switch storage {
@@ -172,6 +173,17 @@ public struct Published<Value> {
}
}
/// Note: This method can mutate `storage`
internal func getPublisher() -> Publisher {
switch storage {
case .value(let value):
let publisher = Publisher(value)
storage = .publisher(publisher)
return publisher
case .publisher(let publisher):
return publisher
}
}
// swiftlint:disable let_var_whitespace
@available(*, unavailable, message: """
@Published is only available on properties of classes
@@ -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
/// services HTML to the downstream publisher and completes normally. Otherwise,
/// the retry operator attempts to reestablish the connection. If after three attempts
/// the publisher still cant connect to the remote URL, the `catch(_:)` operator
/// replaces the error with a new publisher that publishes a connection timed out
/// HTML page. After the downstream subscriber receives the timed out message,
/// the stream completes normally.
///
/// struct WebSiteData: Codable {
/// var rawHTML: String
/// }
///
/// let myURL = URL(string: "https://www.example.com")
///
/// cancellable = URLSession.shared.dataTaskPublisher(for: myURL!)
/// .retry(3)
/// .map { page -> WebSiteData in
/// WebSiteData(rawHTML: String(decoding: page.data, as: UTF8.self))
/// }
/// .catch { error in
/// Just(
/// WebSiteData(
/// rawHTML: "<HTML>Unable to load page - timed out.</HTML>"
/// )
/// )
/// }
/// .sink(receiveCompletion: { print ("completion: \($0)") },
/// receiveValue: { print ("value: \($0)") })
///
/// // Prints: The HTML content from the remote URL upon a successful connection,
/// // or returns "<HTML>Unable to load page - timed out.</HTML>" if
/// // the number of retries exceeds the specified value.
///
/// After exceeding the specified number of retries, the publisher passes the failure
/// to the downstream receiver.
/// - Parameter retries: The number of times to attempt to recreate the subscription.
/// - Returns: A publisher that attempts to recreate its subscription to a failed
/// upstream publisher.
public func retry(_ retries: Int) -> Publishers.Retry<Self> {
return .init(upstream: self, retries: retries)
}
}
extension Publishers {
/// A publisher that attempts to recreate its subscription to a failed upstream
/// publisher.
public struct Retry<Upstream: Publisher>: Publisher {
public typealias Output = Upstream.Output
public typealias Failure = Upstream.Failure
/// The publisher from which this publisher receives elements.
public let upstream: Upstream
/// The maximum number of retry attempts to perform.
///
/// If `nil`, this publisher attempts to reconnect with the upstream publisher
/// an unlimited number of times.
public let retries: Int?
/// Creates a publisher that attempts to recreate its subscription to a failed
/// upstream publisher.
///
/// - Parameters:
/// - upstream: The publisher from which this publisher receives its elements.
/// - retries: The maximum number of retry attempts to perform. If `nil`, this
/// publisher attempts to reconnect with the upstream publisher an unlimited
/// number of times.
public init(upstream: Upstream, retries: Int?) {
self.upstream = upstream
self.retries = retries
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Downstream.Input == Output, Downstream.Failure == Failure
{
upstream.subscribe(Inner(parent: self, downstream: subscriber))
}
}
}
extension Publishers.Retry: Equatable where Upstream: Equatable {}
extension Publishers.Retry {
private final class Inner<Downstream: Subscriber>
: Subscriber,
Subscription,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
where Downstream.Failure == Failure, Downstream.Input == Output
{
typealias Input = Upstream.Output
typealias Failure = Upstream.Failure
private enum State {
case ready(Publishers.Retry<Upstream>, Downstream)
case terminal
}
private enum Chances {
case finite(Int)
case infinite
}
private let lock = UnfairLock.allocate()
private var state: State
private var upstreamSubscription: Subscription?
private var remaining: Chances
private var downstreamNeedsSubscription = true
private var downstreamDemand = Subscribers.Demand.none
private var completionRecursion = false
private var needsSubscribe = false
init(parent: Publishers.Retry<Upstream>, downstream: Downstream) {
state = .ready(parent, downstream)
remaining = parent.retries.map(Chances.finite) ?? .infinite
}
deinit {
lock.deallocate()
}
func receive(subscription: Subscription) {
lock.lock()
guard case let .ready(_, downstream) = state, upstreamSubscription == nil
else {
lock.unlock()
subscription.cancel()
return
}
upstreamSubscription = subscription
let downstreamDemand = self.downstreamDemand
let downstreamNeedsSubscription = self.downstreamNeedsSubscription
self.downstreamNeedsSubscription = false
lock.unlock()
if downstreamNeedsSubscription {
downstream.receive(subscription: self)
}
if downstreamDemand != .none {
subscription.request(downstreamDemand)
}
}
func receive(_ input: Input) -> Subscribers.Demand {
lock.lock()
guard case let .ready(_, downstream) = state else {
lock.unlock()
return .none
}
downstreamDemand -= 1
lock.unlock()
let newDemand = downstream.receive(input)
if newDemand == .none { return .none }
lock.lock()
downstreamDemand += newDemand
if let upstreamSubscription = self.upstreamSubscription {
lock.unlock()
upstreamSubscription.request(newDemand)
} else {
lock.unlock()
}
return .none
}
func receive(completion: Subscribers.Completion<Failure>) {
lock.lock()
guard case let .ready(parent, downstream) = state else {
lock.unlock()
return
}
if case .failure = completion {
upstreamSubscription = nil
switch remaining {
case .finite(0):
break
case .finite(let attempts):
remaining = .finite(attempts - 1)
fallthrough
case .infinite:
if completionRecursion {
needsSubscribe = true
lock.unlock()
return
}
repeat {
completionRecursion = true
needsSubscribe = false
lock.unlock()
parent.upstream.subscribe(self)
lock.lock()
completionRecursion = false
} while needsSubscribe
lock.unlock()
return
}
}
state = .terminal
lock.unlock()
downstream.receive(completion: completion)
}
func request(_ demand: Subscribers.Demand) {
lock.lock()
guard case .ready = state else {
lock.unlock()
return
}
downstreamDemand += demand
if let upstreamSubscription = self.upstreamSubscription {
lock.unlock()
upstreamSubscription.request(demand)
} else {
lock.unlock()
}
}
func cancel() {
lock.lock()
guard case .ready = state else {
lock.unlock()
return
}
state = .terminal
if let upstreamSubscription = self.upstreamSubscription {
lock.unlock()
upstreamSubscription.cancel()
} else {
lock.unlock()
}
}
var description: String { return "Retry" }
var customMirror: Mirror {
return Mirror(self, children: EmptyCollection())
}
var playgroundDescription: Any { return description }
}
}
@@ -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, *)