From f1d45a774d25fe776f68eeb78ceef0ac7e25219f Mon Sep 17 00:00:00 2001 From: tcldr Date: Mon, 24 Jun 2019 18:56:46 +0200 Subject: [PATCH] Initial release --- .../xcshareddata/IDETemplateMacros.plist | 30 +++ .../xcshareddata/xcschemes/EntwineRx.xcscheme | 8 +- .../xcschemes/EntwineRxTests.xcscheme | 23 +- .../xcschemes/xcschememanagement.plist | 4 +- LICENSE | 23 ++ Package.resolved | 6 +- Package.swift | 2 +- README.md | 119 ++++++++- Sources/EntwineRx/BridgeToCombine.swift | 245 ++++++++++++------ Sources/EntwineRx/BridgeToRx.swift | 26 +- Sources/EntwineRx/EntwineRx.swift | 9 - Tests/EntwineRxTests/CombineToRxTests.swift | 48 +++- Tests/EntwineRxTests/RxToCombineTests.swift | 62 +++-- .../XCEntwineRxTestPlan.xctestplan | 23 -- 14 files changed, 478 insertions(+), 150 deletions(-) create mode 100644 .swiftpm/xcode/package.xcworkspace/xcshareddata/IDETemplateMacros.plist create mode 100644 LICENSE delete mode 100644 Sources/EntwineRx/EntwineRx.swift delete mode 100644 Tests/EntwineRxTests/XCEntwineRxTestPlan.xctestplan diff --git a/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDETemplateMacros.plist b/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDETemplateMacros.plist new file mode 100644 index 0000000..3e25acb --- /dev/null +++ b/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDETemplateMacros.plist @@ -0,0 +1,30 @@ + + + + + FILEHEADER + +// ___WORKSPACENAME___ +// https://github.com/tcldr/___WORKSPACENAME___ +// +// Copyright © ___YEAR___ Tristan Celder. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + + diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/EntwineRx.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/EntwineRx.xcscheme index 71931bd..e1be725 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/EntwineRx.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/EntwineRx.xcscheme @@ -22,10 +22,10 @@ + buildForRunning = "NO" + buildForProfiling = "NO" + buildForArchiving = "NO" + buildForAnalyzing = "NO"> + + + + + + + shouldUseLaunchSchemeArgsEnv = "YES"> - - EntwineRx.xcscheme_^#shared#^_ orderHint - 0 + 3 EntwineRxTests.xcscheme_^#shared#^_ orderHint - 2 + 4 SuppressBuildableAutocreation diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..34ac2d3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,23 @@ + +EntwineRx +https://github.com/tcldr/EntwineRx + +Copyright © 2019 Tristan Celder. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/Package.resolved b/Package.resolved index f8866ca..0b43b59 100644 --- a/Package.resolved +++ b/Package.resolved @@ -5,9 +5,9 @@ "package": "Entwine", "repositoryURL": "http://github.com/tcldr/Entwine.git", "state": { - "branch": "master", - "revision": "ae85f1a86bb83e9ed10a54c50de33ed9efafea5b", - "version": null + "branch": null, + "revision": "5c26eac6c9c4e97d65d9c42eea355ec08f9e36e8", + "version": "0.1.0" } }, { diff --git a/Package.swift b/Package.swift index e256cac..7dc4e55 100644 --- a/Package.swift +++ b/Package.swift @@ -15,7 +15,7 @@ let package = Package( ], dependencies: [ // Dependencies declare other packages that this package depends on. - .package(url: "http://github.com/tcldr/Entwine.git", .branch("master")), + .package(url: "http://github.com/tcldr/Entwine.git", .upToNextMinor(from: "0.1.0")), .package(url: "http://github.com/ReactiveX/RxSwift.git", .upToNextMajor(from: "5.0.0")), ], targets: [ diff --git a/README.md b/README.md index ac809ad..c426cd2 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,120 @@ + # EntwineRx -A description of this package. +Part of [Entwine](https://github.com/tcldr/Entwine) – A collection of accessories for [Apple's Combine Framework](https://developer.apple.com/documentation/combine). + +--- + +### CONTENTS +- [About EnwtineRx](#about-entwinerx) +- [Getting Started](#getting-started) +- [Installation](#installation) +- [Documentation](#documentation) +- [Copyright and License](#copyright-and-license) + +--- + +### ABOUT ENTWINE RX +_EntwineRx_ is a lightweight library of two operators to bridge from _RxSwift_ to _Combine_ and back again. You can use _EntwineRx_ to make your existing _RxSwift_ view models and operators work seamlessly with any _Combine_ based code you might be developing. + +be sure to checkout the [documentation](http://tcldr.github.io/Entwine/EntwineDocs) for the full list of operators and utilities. + +--- + +### GETTING STARTED + +## From _Combine_ to _RxSwift_ +The `bridgeToRx()` operator that _EntwineRx_ adds to _Combine_'s `Publisher` type means that you can bridge to _RxSwift_ as simply as: + +```swift +import Combine +import RxSwift +import EntwineRx + +let myRxObservable = Publishers.Just(1).bridgeToRx() + +``` +## From _RxSwift_ to _Combine_ +This direction requires a little more thought as we now have to account for _Combine_'s requirement that any publisher obeys its [strict back pressure requirements](#backpressure-and-combine). + +As _RxSwift_ has no such restriction we have to give some guidance for how we would like any unrequested elements from our `Observable` stream to be handled. We do this by introducing an element buffer to store our elements until they're requested by a subscriber. + +We must then decide: +1. How big that buffer should be +2. What we should do when our buffer is full + +As long as we are able to reason about the source of our upstream `Observable`'s elements, and the rate at which we expect our subscriber to consume them – this should be relatively straightforward. + +In this case we can safely set a low buffer count, and fail if we enter a situation that is unexpected. (If failing is not an option, you can also choose to drop any new elements or drop the buffer's oldest elements.) + +So what number? At least one. And one is often enough. A sequence would require a buffer size of at least the count of the sequence. + +```swift +import Combine +import RxSwift +import EntwineRx + +// both `.just(_)` and `from(_)` deliver their elements immediately upon subscription – we need a buffer! +let combinePublisher1 = Observable.just(1) + .bridgeToCombine(bufferSize: 1, whenFull: .fail) + +let combinePublisher2 = Observable.from([1,2,3,4,]) + .bridgeToCombine(bufferSize: 4, whenFull: .fail) + +``` +If the buffer _does_ overflow the publisher will complete with an `RxBridgeFailure.bufferOverflow` failure. From here you can decide how you wish to recover. + +If you're feeling especially self-assured you can chain the `.assertBridgeBufferNeverOverflows()` operator after the bridge operator. Be warned though – as the name suggests this will cause an assertion failure if your hypothesis proves incorrect. + +--- + +### BACKPRESSURE AND _COMBINE_ + +Backpressure is the situation in which a publisher is producing elements faster than a subscriber can consume. + +This behavior can result in a scenario where unconsumed elements are building up in the buffer of some publisher faster than they can be processed by a downstream subscriber. This can very quickly consume system resources causing performance degradation – and ultimately out-of-memory errors. + +_Combine_ handles backpressure by only allowing a publisher to emit an element if it is has been signalled by its subscriber. A subscriber can signal at any point during the lifetime of a subscription, and may decide not to signal at all. Therefore it is the responsibility of the publisher to limit its output accordingly. + +--- + +### INSTALLATION +### As part of another Swift Package: +1. Include it in your `Package.swift` file as both a dependency and a dependency of your target. + +```swift +import PackageDescription + +let package = Package( + ... + dependencies: [ + .package(url: "http://github.com/tcldr/EntwineRx.git", .upToNextMajor(from: "0.1.0")), + ], + ... + targets: [.target(name: "MyTarget", dependencies: ["EntwineRx"]), + ] +) +``` + +2. Then run `swift package update` from the root directory of your SPM project. If you're using Xcode 11 to edit your SPM project this should happen automatically. + +### As part of an Xcode 11 or greater project: +1. Select the `File -> Swift Packages -> Add package dependency...` menu item. +2. Enter the repository url `https://github.com/tcldr/EntwineRx` and tap next. +3. Select 'version, 'up to next major', enter `0.1.0`, hit next. +4. Select the _Entwine_ library and specify the target you wish to use it with. + + +--- + +### DOCUMENTATION +Full documentation for _EntwineRx_ can be found at [http://tcldr.github.io/Entwine/EntwineRxDocs](http://tcldr.github.io/Entwine/EntwineRxDocs). + +--- + +### COPYRIGHT AND LICENSE +Copyright 2019 © Tristan Celder + +_EntwineRx_ is made available under the [MIT License](http://github.com/tcldr/Entwine/blob/master/LICENSE) + +--- \ No newline at end of file diff --git a/Sources/EntwineRx/BridgeToCombine.swift b/Sources/EntwineRx/BridgeToCombine.swift index a7d93ff..1561bcc 100644 --- a/Sources/EntwineRx/BridgeToCombine.swift +++ b/Sources/EntwineRx/BridgeToCombine.swift @@ -1,101 +1,192 @@ // -// File.swift -// +// EntwineRx +// https://github.com/tcldr/EntwineRx // -// Created by Tristan Celder on 17/06/2019. +// Copyright © 2019 Tristan Celder. All rights reserved. // +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. import Combine import RxSwift +extension ObservableType { + + /// A bridging operator that transforms an RxSwift `ObservableType` into a Combine `Publisher`. + /// + /// Note: A legal combine `Publisher` must limit its production to the level specified by a `Subscriber` + /// whereas an RxSwift `Observable` has no such restriction. Therefore, this operator maintains a buffer + /// of elements from the upstream `Observable` to store until requested. + /// + /// To avoid out-of-memory errors, and constrain resource usage in general, it is recommended to provide + /// a conservative value for `bufferSize` matching the expected output of the upstream `Observable` + /// and consumption of the downstream `Subscriber`. + /// + /// - Parameter bufferSize: The size of the element buffer. + /// - Parameter whenFull: The strategy to employ for handling subsequent elements if the buffer + /// reaches capacity + public func bridgeToCombine(bufferSize: Int, whenFull: RxBridgeBufferingStrategy = .fail) -> Publishers.Buffer> { + RxBridge(upstream: self).buffer(size: bufferSize, prefetch: .byRequest, whenFull: whenFull.bufferingStrategy ) + } +} + +/// `Failure` type of the `RxBridge` publisher. +/// +/// Either: +/// - An `Error` forwarded from a upstream `Observable`, or +/// – A notice that the publisher terminated the subscription due to the upstream `Observable` emitting at a greater +/// rate than was specified when the publisher was created. +/// public enum RxBridgeFailure: Error { case upstreamError(Error) case bufferOverflow } -extension Publishers { +/// Buffering strategy options for the `.bridgeToCombine(bufferSize:whenFull:)` operator. +/// +/// When the buffer is full, for subsequent upstream elements: +/// - `.dropNewest`: will discard additional elements. +/// - `.dropOldest`: will purge the oldest element and append the new element to the buffer. +/// - `.fail`: will complete the `Publisher` with a `RxBridgeFailure.bufferOverflow` and +/// end the sequence. +/// +public enum RxBridgeBufferingStrategy { + case dropNewest + case dropOldest + case fail +} + +// MARK: - Publisher definition + +public struct RxBridge: Publisher { - public struct RxBridge: Publisher { - - public typealias Output = Upstream.Element - public typealias Failure = RxBridgeFailure - - let upstream: Upstream - - init(upstream: Upstream) { - self.upstream = upstream - } - - public func receive(subscriber: S) where Failure == S.Failure, Output == S.Input { - subscriber.receive(subscription: RxBridgeSubscription(upstream: upstream, downstream: subscriber)) - } + public typealias Output = Upstream.Element + public typealias Failure = RxBridgeFailure + + let upstream: Upstream + + init(upstream: Upstream) { + self.upstream = upstream + } + + public func receive(subscriber: S) where Failure == S.Failure, Output == S.Input { + subscriber.receive(subscription: RxBridgeSubscription(upstream: upstream, downstream: subscriber)) } +} + +// MARK: - Publisher subscription + +fileprivate final class RxBridgeSubscription: Subscription where Upstream.Element == Downstream.Input, Downstream.Failure == RxBridgeFailure { - fileprivate final class RxBridgeSubscription: Subscription where Upstream.Element == Downstream.Input, Downstream.Failure == RxBridgeFailure { - - enum Status { - case pending - case active(RxBridgeSink) - case complete - } - - let upstream: Upstream - let downstream: Downstream - var status = Status.pending - - init(upstream: Upstream, downstream: Downstream) { - self.upstream = upstream - self.downstream = downstream - } - - // There's nothing we can do to satisfy a request for a finite demand - // as RxSwift doesn't have intrinsic backpressure support. However, if - // we _know_ that the subscriber is in fact a `Buffer` sink (enforced - // by the API keeping the initialiser to the operator internal, and - // only exposing the `ObservableType` factory method) we can guarantee - // legal behavior. Now, our only responsibility is to ensure we only - // start the subscription when we reach a demand threshold of one - func request(_ demand: Subscribers.Demand) { - guard case .pending = status, demand > .none else { return } - status = .active(RxBridgeSink(upstream: upstream, downstream: downstream)) - } - - func cancel() { - status = .complete - } + enum Status { + case pending + case active(RxBridgeSink) + case complete } - fileprivate final class RxBridgeSink: ObserverType where Upstream.Element == Downstream.Input, Downstream.Failure == RxBridgeFailure { - - typealias Element = Upstream.Element - - let downstream: Downstream - var disposable: Disposable? - - init(upstream: Upstream, downstream: Downstream) { - self.downstream = downstream - self.disposable = upstream.subscribe(self) - } - - deinit { - disposable?.dispose() + let upstream: Upstream + let downstream: Downstream + var status = Status.pending + + init(upstream: Upstream, downstream: Downstream) { + self.upstream = upstream + self.downstream = downstream + } + + // There's nothing we can do to satisfy a request for a finite demand + // as RxSwift doesn't have intrinsic backpressure support. However, if + // we _know_ that the subscriber is in fact a `Buffer` sink (enforced + // by the API keeping the initialiser to the operator internal, and + // only exposing the `ObservableType` factory method) we can guarantee + // legal behavior. Now, our only responsibility is to ensure we only + // start the subscription when we reach a demand threshold of one + func request(_ demand: Subscribers.Demand) { + guard case .pending = status, demand > .none else { return } + status = .active(RxBridgeSink(upstream: upstream, downstream: downstream)) + } + + func cancel() { + status = .complete + } +} + +// MARK: - Publisher Sink + +fileprivate final class RxBridgeSink: ObserverType where Upstream.Element == Downstream.Input, Downstream.Failure == RxBridgeFailure { + + typealias Element = Upstream.Element + + let downstream: Downstream + var disposable: Disposable? + + init(upstream: Upstream, downstream: Downstream) { + self.downstream = downstream + self.disposable = upstream.subscribe(self) + } + + deinit { + disposable?.dispose() + } + + func on(_ event: Event) { + switch event { + case .next(let value): + _ = downstream.receive(value) + case .error(let error): + downstream.receive(completion: .failure(.upstreamError(error))) + case .completed: + downstream.receive(completion: .finished) } - - func on(_ event: Event) { - switch event { - case .next(let value): - _ = downstream.receive(value) - case .error(let error): - downstream.receive(completion: .failure(.upstreamError(error))) - case .completed: - downstream.receive(completion: .finished) - } + } +} + +// MARK: - RxBridgeBufferingStrategy to Publishers.BufferingStrategy + +extension RxBridgeBufferingStrategy { + var bufferingStrategy: Publishers.BufferingStrategy { + switch self { + case .fail: + return .customError { .bufferOverflow } + case .dropNewest: + return .dropNewest + case .dropOldest: + return .dropOldest } } } -extension ObservableType { - public func bridgeToCombine() -> Publishers.Buffer> { - Publishers.RxBridge(upstream: self).buffer(size: 1, prefetch: .byRequest, whenFull: .customError({ return RxBridgeFailure.bufferOverflow })) +// MARK: - Publisher RxBridgeFailure assertion + +extension Publisher where Failure == RxBridgeFailure { + + /// Raises a fatal error when an upstream `RxBridge` publisher's buffer overflows and otherwise maps the failure + /// type to the `Error` type of an upstream `Observable`. + /// + /// Use this operator at some point following the `RxBridge` operator if you can be sure that a buffer overflow + /// will never occur. + /// + /// - Returns: A publisher with an Failure type that matches the `Error` type of an upstream bridged `Observable` + public func assertBridgeBufferNeverOverflows() -> Publishers.MapError { + return mapError { error -> Error in + guard case .upstreamError(let e) = error else { + preconditionFailure("RxBridge buffer overflowed.") + } + return e + } } } diff --git a/Sources/EntwineRx/BridgeToRx.swift b/Sources/EntwineRx/BridgeToRx.swift index aef4d23..2957132 100644 --- a/Sources/EntwineRx/BridgeToRx.swift +++ b/Sources/EntwineRx/BridgeToRx.swift @@ -1,14 +1,34 @@ // -// File.swift -// +// EntwineRx +// https://github.com/tcldr/EntwineRx // -// Created by Tristan Celder on 17/06/2019. +// Copyright © 2019 Tristan Celder. All rights reserved. // +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. import Combine import RxSwift extension Publisher { + + /// A bridging operator that transforms a Combine `Publisher` into an RxSwift `Observable`. + /// - Returns: An observable that republishes the elements of an upstream Combine publisher public func bridgeToRx() -> Observable { Observable.create { observer in let cancellable = self.sink( diff --git a/Sources/EntwineRx/EntwineRx.swift b/Sources/EntwineRx/EntwineRx.swift deleted file mode 100644 index ddb32e2..0000000 --- a/Sources/EntwineRx/EntwineRx.swift +++ /dev/null @@ -1,9 +0,0 @@ -// -// File.swift -// -// -// Created by Tristan Celder on 17/06/2019. -// - -//import Combine -import RxSwift diff --git a/Tests/EntwineRxTests/CombineToRxTests.swift b/Tests/EntwineRxTests/CombineToRxTests.swift index 6d0679f..fbd46c4 100644 --- a/Tests/EntwineRxTests/CombineToRxTests.swift +++ b/Tests/EntwineRxTests/CombineToRxTests.swift @@ -1,9 +1,26 @@ // -// File.swift -// +// EntwineRx +// https://github.com/tcldr/EntwineRx // -// Created by Tristan Celder on 17/06/2019. +// Copyright © 2019 Tristan Celder. All rights reserved. // +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. import XCTest import Combine @@ -48,7 +65,26 @@ final class CombineToRxTests: XCTestCase { XCTAssertEqual(expected, results1.events) } - static var allTests = [ - ("testBasicCase", testBasicCase), - ] + func testCompletePropagatesDownstream() { + + let disposeBag = DisposeBag() + let results1 = scheduler.createObserver(Int.self) + let subject = PassthroughSubject() + + scheduler.scheduleAt(100) { subject.bridgeToRx().subscribe(results1).disposed(by: disposeBag) } + scheduler.scheduleAt(200) { subject.send(0) } + scheduler.scheduleAt(300) { subject.send(1) } + scheduler.scheduleAt(400) { subject.send(completion: .finished) } + + let expected = [ + Recorded.next(200, 0), + Recorded.next(300, 1), + Recorded.completed(400) + ] + + scheduler.start() + + XCTAssertEqual(expected, results1.events) + + } } diff --git a/Tests/EntwineRxTests/RxToCombineTests.swift b/Tests/EntwineRxTests/RxToCombineTests.swift index fa6c8be..746d6cc 100644 --- a/Tests/EntwineRxTests/RxToCombineTests.swift +++ b/Tests/EntwineRxTests/RxToCombineTests.swift @@ -1,9 +1,26 @@ // -// File.swift -// +// EntwineRx +// https://github.com/tcldr/EntwineRx // -// Created by Tristan Celder on 17/06/2019. +// Copyright © 2019 Tristan Celder. All rights reserved. // +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. import XCTest import Combine @@ -30,26 +47,41 @@ final class RxToCombineTests: XCTestCase { let results1 = scheduler.createTestableSubscriber(Int.self, Never.self) let subject = PublishSubject() - let sut = subject.bridgeToCombine().mapError { _ -> Never in fatalError() } + let sut = subject.bridgeToCombine(bufferSize: 1).assertNoFailure() scheduler.schedule(after: 100) { sut.subscribe(results1) } scheduler.schedule(after: 200) { subject.onNext(0) } scheduler.schedule(after: 300) { subject.onNext(1) } scheduler.schedule(after: 400) { subject.onNext(2) } - let expected: [TestableSubscriberEvent] = [ - .init(100, .subscribe), - .init(200, .input(0)), - .init(300, .input(1)), - .init(400, .input(2)), - ] - scheduler.resume() - XCTAssertEqual(expected, results1.events) + XCTAssertEqual(results1.sequence, [ + (100, .subscription), + (200, .input(0)), + (300, .input(1)), + (400, .input(2)), + ]) } - static var allTests = [ - ("testBasicCase", testBasicCase), - ] + func testCompletePropagatesDownstream() { + + let results1 = scheduler.createTestableSubscriber(Int.self, Never.self) + let subject = PublishSubject() + let sut = subject.bridgeToCombine(bufferSize: 1).assertNoFailure() + + scheduler.schedule(after: 100) { sut.subscribe(results1) } + scheduler.schedule(after: 200) { subject.onNext(0) } + scheduler.schedule(after: 300) { subject.onNext(1) } + scheduler.schedule(after: 400) { subject.onCompleted() } + + scheduler.resume() + + XCTAssertEqual(results1.sequence, [ + (100, .subscription), + (200, .input(0)), + (300, .input(1)), + (400, .completion(.finished)), + ]) + } } diff --git a/Tests/EntwineRxTests/XCEntwineRxTestPlan.xctestplan b/Tests/EntwineRxTests/XCEntwineRxTestPlan.xctestplan deleted file mode 100644 index 3284096..0000000 --- a/Tests/EntwineRxTests/XCEntwineRxTestPlan.xctestplan +++ /dev/null @@ -1,23 +0,0 @@ -{ - "configurations" : [ - { - "name" : "Configuration 1", - "options" : { - - } - } - ], - "defaultOptions" : { - - }, - "testTargets" : [ - { - "target" : { - "containerPath" : "container:", - "identifier" : "EntwineRxTests", - "name" : "EntwineRxTests" - } - } - ], - "version" : 1 -} \ No newline at end of file