diff --git a/RemainingCombineInterface.swift b/RemainingCombineInterface.swift index 890557b..147b712 100644 --- a/RemainingCombineInterface.swift +++ b/RemainingCombineInterface.swift @@ -1328,111 +1328,6 @@ extension Publisher { public func throttle(for interval: S.SchedulerTimeType.Stride, scheduler: S, latest: Bool) -> Publishers.Throttle where S : Scheduler } -extension Publishers { - - /// A publisher that republishes items from another publisher only if each new item is in increasing order from the previously-published item. - public struct Comparison : 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 that this publisher receives elements from. - public let upstream: Upstream - - /// A closure that receives two elements and returns `true` if they are in increasing order. - public let areInIncreasingOrder: (Upstream.Output, Upstream.Output) -> Bool - - public init(upstream: Upstream, areInIncreasingOrder: @escaping (Upstream.Output, Upstream.Output) -> Bool) - - /// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)` - /// - /// - SeeAlso: `subscribe(_:)` - /// - Parameters: - /// - subscriber: The subscriber to attach to this `Publisher`. - /// once attached it can begin to receive values. - public func receive(subscriber: S) where S : Subscriber, Upstream.Failure == S.Failure, Upstream.Output == S.Input - } - - /// A publisher that republishes items from another publisher only if each new item is in increasing order from the previously-published item, and fails if the ordering logic throws an error. - public struct TryComparison : 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 = Error - - /// The publisher that this publisher receives elements from. - public let upstream: Upstream - - /// A closure that receives two elements and returns `true` if they are in increasing order. - public let areInIncreasingOrder: (Upstream.Output, Upstream.Output) throws -> Bool - - public init(upstream: Upstream, areInIncreasingOrder: @escaping (Upstream.Output, Upstream.Output) throws -> Bool) - - /// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)` - /// - /// - SeeAlso: `subscribe(_:)` - /// - Parameters: - /// - subscriber: The subscriber to attach to this `Publisher`. - /// once attached it can begin to receive values. - public func receive(subscriber: S) where S : Subscriber, Upstream.Output == S.Input, S.Failure == Publishers.TryComparison.Failure - } -} - -extension Publisher where Self.Output : Comparable { - - /// Publishes the minimum value received from the upstream publisher, after it finishes. - /// - /// After this publisher receives a request for more than 0 items, it requests unlimited items from its upstream publisher. - /// - Returns: A publisher that publishes the minimum value received from the upstream publisher, after the upstream publisher finishes. - public func min() -> Publishers.Comparison - - /// Publishes the maximum value received from the upstream publisher, after it finishes. - /// - /// After this publisher receives a request for more than 0 items, it requests unlimited items from its upstream publisher. - /// - Returns: A publisher that publishes the maximum value received from the upstream publisher, after the upstream publisher finishes. - public func max() -> Publishers.Comparison -} - -extension Publisher { - - /// Publishes the minimum value received from the upstream publisher, after it finishes. - /// - /// After this publisher receives a request for more than 0 items, it requests unlimited items from its upstream publisher. - /// - Parameter areInIncreasingOrder: A closure that receives two elements and returns `true` if they are in increasing order. - /// - Returns: A publisher that publishes the minimum value received from the upstream publisher, after the upstream publisher finishes. - public func min(by areInIncreasingOrder: @escaping (Self.Output, Self.Output) -> Bool) -> Publishers.Comparison - - /// Publishes the minimum value received from the upstream publisher, using the provided error-throwing closure to order the items. - /// - /// After this publisher receives a request for more than 0 items, it requests unlimited items from its upstream publisher. - /// - Parameter areInIncreasingOrder: A throwing closure that receives two elements and returns `true` if they are in increasing order. If this closure throws, the publisher terminates with a `Failure`. - /// - Returns: A publisher that publishes the minimum value received from the upstream publisher, after the upstream publisher finishes. - public func tryMin(by areInIncreasingOrder: @escaping (Self.Output, Self.Output) throws -> Bool) -> Publishers.TryComparison - - /// Publishes the maximum value received from the upstream publisher, using the provided ordering closure. - /// - /// After this publisher receives a request for more than 0 items, it requests unlimited items from its upstream publisher. - /// - Parameter areInIncreasingOrder: A closure that receives two elements and returns `true` if they are in increasing order. - /// - Returns: A publisher that publishes the maximum value received from the upstream publisher, after the upstream publisher finishes. - public func max(by areInIncreasingOrder: @escaping (Self.Output, Self.Output) -> Bool) -> Publishers.Comparison - - /// Publishes the maximum value received from the upstream publisher, using the provided error-throwing closure to order the items. - /// - /// After this publisher receives a request for more than 0 items, it requests unlimited items from its upstream publisher. - /// - Parameter areInIncreasingOrder: A throwing closure that receives two elements and returns `true` if they are in increasing order. If this closure throws, the publisher terminates with a `Failure`. - /// - Returns: A publisher that publishes the maximum value received from the upstream publisher, after the upstream publisher finishes. - public func tryMax(by areInIncreasingOrder: @escaping (Self.Output, Self.Output) throws -> Bool) -> Publishers.TryComparison -} - extension Publishers { /// A publisher that replaces an empty stream with a provided element. diff --git a/Sources/OpenCombine/Publishers/Publishers.Comparison.swift b/Sources/OpenCombine/Publishers/Publishers.Comparison.swift new file mode 100644 index 0000000..d27a9a1 --- /dev/null +++ b/Sources/OpenCombine/Publishers/Publishers.Comparison.swift @@ -0,0 +1,246 @@ +// +// Publishers.Comparison.swift +// OpenCombine +// +// Created by Ilija Puaca on 22/7/19. +// + +extension Publisher where Output: Comparable { + + /// Publishes the minimum value received from the upstream publisher, after it + /// finishes. + /// + /// After this publisher receives a request for more than 0 items, it requests + /// unlimited items from its upstream publisher. + /// + /// - Returns: A publisher that publishes the minimum value received from the upstream + /// publisher, after the upstream publisher finishes. + public func min() -> Publishers.Comparison { + return max(by: >) + } + + /// Publishes the maximum value received from the upstream publisher, after it + /// finishes. + /// + /// After this publisher receives a request for more than 0 items, it requests + /// unlimited items from its upstream publisher. + /// + /// - Returns: A publisher that publishes the maximum value received from the upstream + /// publisher, after the upstream publisher finishes. + public func max() -> Publishers.Comparison { + return max(by: <) + } +} + +extension Publisher { + + /// Publishes the minimum value received from the upstream publisher, after it + /// finishes. + /// + /// After this publisher receives a request for more than 0 items, it requests + /// unlimited items from its upstream publisher. + /// + /// - Parameter areInIncreasingOrder: A closure that receives two elements and returns + /// `true` if they are in increasing order. + /// - Returns: A publisher that publishes the minimum value received from the upstream + /// publisher, after the upstream publisher finishes. + public func min( + by areInIncreasingOrder: @escaping (Output, Output) -> Bool + ) -> Publishers.Comparison { + return max(by: { areInIncreasingOrder($1, $0) }) + } + + /// Publishes the minimum value received from the upstream publisher, using the + /// provided error-throwing closure to order the items. + /// + /// After this publisher receives a request for more than 0 items, it requests + /// unlimited items from its upstream publisher. + /// + /// - Parameter areInIncreasingOrder: A throwing closure that receives two elements + /// and returns `true` if they are in increasing order. If this closure throws, the + /// publisher terminates with a `Failure`. + /// - Returns: A publisher that publishes the minimum value received from the upstream + /// publisher, after the upstream publisher finishes. + public func tryMin( + by areInIncreasingOrder: @escaping (Output, Output) throws -> Bool + ) -> Publishers.TryComparison { + return tryMax(by: { try areInIncreasingOrder($1, $0) }) + } + + /// Publishes the maximum value received from the upstream publisher, using the + /// provided ordering closure. + /// + /// After this publisher receives a request for more than 0 items, it requests + /// unlimited items from its upstream publisher. + /// + /// - Parameter areInIncreasingOrder: A closure that receives two elements and returns + /// `true` if they are in increasing order. + /// - Returns: A publisher that publishes the maximum value received from the upstream + /// publisher, after the upstream publisher finishes. + public func max( + by areInIncreasingOrder: @escaping (Output, Output) -> Bool + ) -> Publishers.Comparison { + return .init(upstream: self, areInIncreasingOrder: areInIncreasingOrder) + } + + /// Publishes the maximum value received from the upstream publisher, using the + /// provided error-throwing closure to order the items. + /// + /// After this publisher receives a request for more than 0 items, it requests + /// unlimited items from its upstream publisher. + /// - Parameter areInIncreasingOrder: A throwing closure that receives two elements + /// and returns `true` if they are in increasing order. If this closure throws, the + /// publisher terminates with a `Failure`. + /// - Returns: A publisher that publishes the maximum value received from the upstream + /// publisher, after the upstream publisher finishes. + public func tryMax( + by areInIncreasingOrder: @escaping (Self.Output, Self.Output) throws -> Bool + ) -> Publishers.TryComparison { + return .init(upstream: self, areInIncreasingOrder: areInIncreasingOrder) + } +} + +extension Publishers { + + /// A publisher that republishes items from another publisher only if each new item is + /// in increasing order from the previously-published item. + public struct Comparison: Publisher { + + public typealias Output = Upstream.Output + + public typealias Failure = Upstream.Failure + + /// The publisher that this publisher receives elements from. + public let upstream: Upstream + + /// A closure that receives two elements and returns `true` if they are in + /// increasing order. + public let areInIncreasingOrder: (Upstream.Output, Upstream.Output) -> Bool + + public init( + upstream: Upstream, + areInIncreasingOrder: @escaping (Upstream.Output, Upstream.Output) -> Bool + ) { + self.upstream = upstream + self.areInIncreasingOrder = areInIncreasingOrder + } + + public func receive(subscriber: Downstream) + where Upstream.Failure == Downstream.Failure, + Upstream.Output == Downstream.Input + { + let inner = Inner(downstream: subscriber, + areInIncreasingOrder: areInIncreasingOrder) + upstream.subscribe(inner) + } + } + + /// A publisher that republishes items from another publisher only if each new item is + /// in increasing order from the previously-published item, and fails if the ordering + /// logic throws an error. + public struct TryComparison: Publisher { + + public typealias Output = Upstream.Output + + public typealias Failure = Error + + /// The publisher that this publisher receives elements from. + public let upstream: Upstream + + /// A closure that receives two elements and returns `true` if they are in + /// increasing order. + public let areInIncreasingOrder: (Upstream.Output, Upstream.Output) throws -> Bool + + public init( + upstream: Upstream, + areInIncreasingOrder: + @escaping (Upstream.Output, Upstream.Output) throws -> Bool + ) { + self.upstream = upstream + self.areInIncreasingOrder = areInIncreasingOrder + } + + public func receive(subscriber: Downstream) + where Upstream.Output == Downstream.Input, Downstream.Failure == Error + { + let inner = Inner(downstream: subscriber, + areInIncreasingOrder: areInIncreasingOrder) + upstream.subscribe(inner) + } + } +} + +extension Publishers.Comparison { + private final class Inner + : ReduceProducer Bool> + where Downstream.Input == Upstream.Output, Downstream.Failure == Upstream.Failure + { + fileprivate init( + downstream: Downstream, + areInIncreasingOrder: @escaping (Upstream.Output, Upstream.Output) -> Bool + ) { + super.init(downstream: downstream, initial: nil, reduce: areInIncreasingOrder) + } + + override func receive( + newValue: Upstream.Output + ) -> PartialCompletion { + if let result = self.result { + if reduce(result, newValue) { + self.result = newValue + } + } else { + self.result = newValue + } + return .continue + } + + override var description: String { + return "Comparison" + } + } +} + +extension Publishers.TryComparison { + private final class Inner + : ReduceProducer Bool> + where Downstream.Input == Upstream.Output, Downstream.Failure == Error + { + fileprivate init( + downstream: Downstream, + areInIncreasingOrder: + @escaping (Upstream.Output, Upstream.Output) throws -> Bool + ) { + super.init(downstream: downstream, initial: nil, reduce: areInIncreasingOrder) + } + + override func receive( + newValue: Upstream.Output + ) -> PartialCompletion { + do { + if let result = self.result { + if try reduce(result, newValue) { + self.result = newValue + } + } else { + self.result = newValue + } + return .continue + } catch { + return .failure(error) + } + } + + override var description: String { + return "TryComparison" + } + } +} diff --git a/Tests/OpenCombineTests/PublisherTests/ComparisonTests.swift b/Tests/OpenCombineTests/PublisherTests/ComparisonTests.swift new file mode 100644 index 0000000..7813281 --- /dev/null +++ b/Tests/OpenCombineTests/PublisherTests/ComparisonTests.swift @@ -0,0 +1,367 @@ +// +// ComparisonTests.swift +// +// +// Created by Sergej Jaskiewicz on 15.10.2019. +// + +import XCTest + +#if OPENCOMBINE_COMPATIBILITY_TEST +import Combine +#else +import OpenCombine +#endif + +@available(macOS 10.15, iOS 13.0, *) +final class ComparisonTests: XCTestCase { + + // MARK: - Comparison + + func testComparisonBasicBehavior() { + ComparisonTests.testBasicBehavior( + expectedSubscription: "Comparison", + expectedResult: 15, + semantics: .max, + countComparatorCalls: false, + { upstream, _ in upstream.max() } + ) + + ComparisonTests.testBasicBehavior( + expectedSubscription: "Comparison", + expectedResult: 1, + semantics: .min, + countComparatorCalls: false, + { upstream, _ in upstream.min() } + ) + + ComparisonTests.testBasicBehavior( + expectedSubscription: "Comparison", + expectedResult: 8, + semantics: .max, + { upstream, comparator in upstream.max(by: comparator) } + ) + + ComparisonTests.testBasicBehavior( + expectedSubscription: "Comparison", + expectedResult: 1, + semantics: .min, + { upstream, comparator in upstream.min(by: comparator) } + ) + } + + func testComparisonUpstreamFinishesWithError() { + ReduceTests.testUpstreamFinishesWithError(expectedSubscription: "Comparison", + { $0.max() }) + + ReduceTests.testUpstreamFinishesWithError(expectedSubscription: "Comparison", + { $0.min() }) + + ReduceTests.testUpstreamFinishesWithError(expectedSubscription: "Comparison", + { $0.max(by: shouldNotBeCalled()) }) + + ReduceTests.testUpstreamFinishesWithError(expectedSubscription: "Comparison", + { $0.min(by: shouldNotBeCalled()) }) + } + + func testComparisonUpstreamFinishesImmediately() { + ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "Comparison", + expectedResult: nil, + { $0.max() }) + + ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "Comparison", + expectedResult: nil, + { $0.min() }) + + ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "Comparison", + expectedResult: nil, + { $0.max(by: shouldNotBeCalled()) }) + + ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "Comparison", + expectedResult: nil, + { $0.min(by: shouldNotBeCalled()) }) + } + + func testComparisonCancelAlreadyCancelled() throws { + try ReduceTests.testCancelAlreadyCancelled { $0.max() } + try ReduceTests.testCancelAlreadyCancelled { $0.min() } + try ReduceTests.testCancelAlreadyCancelled { $0.max(by: shouldNotBeCalled()) } + try ReduceTests.testCancelAlreadyCancelled { $0.min(by: shouldNotBeCalled()) } + } + + func testComparisonRequestsUnlimitedThenSendsSubscription() { + ReduceTests.testRequestsUnlimitedThenSendsSubscription { $0.max() } + + ReduceTests.testRequestsUnlimitedThenSendsSubscription { $0.min() } + + ReduceTests.testRequestsUnlimitedThenSendsSubscription { + $0.max(by: shouldNotBeCalled()) + } + + ReduceTests.testRequestsUnlimitedThenSendsSubscription { + $0.min(by: shouldNotBeCalled()) + } + } + + func testComparisonReceiveSubscriptionTwice() throws { + try ReduceTests.testReceiveSubscriptionTwice(expectedSubscription: "Comparison", + expectedResult: .normalCompletion(0), + { $0.max() }) + + try ReduceTests.testReceiveSubscriptionTwice(expectedSubscription: "Comparison", + expectedResult: .normalCompletion(0), + { $0.min() }) + + try ReduceTests.testReceiveSubscriptionTwice(expectedSubscription: "Comparison", + expectedResult: .normalCompletion(0), + { $0.max(by: shouldNotBeCalled()) }) + + try ReduceTests.testReceiveSubscriptionTwice(expectedSubscription: "Comparison", + expectedResult: .normalCompletion(0), + { $0.min(by: shouldNotBeCalled()) }) + } + + func testComparisonLifecycle() throws { + try testLifecycle(sendValue: 31, + cancellingSubscriptionReleasesSubscriber: false, + { $0.min(by: >) }) + + try testLifecycle(sendValue: 31, + cancellingSubscriptionReleasesSubscriber: false, + { $0.max(by: >) }) + } + + func testComparisonReflection() throws { + try testReflection(parentInput: Int.self, + parentFailure: Error.self, + description: "Comparison", + customMirror: reduceLikeOperatorMirror(), + playgroundDescription: "Comparison", + { $0.min(by: shouldNotBeCalled()) }) + + try testReflection(parentInput: Int.self, + parentFailure: Error.self, + description: "Comparison", + customMirror: reduceLikeOperatorMirror(), + playgroundDescription: "Comparison", + { $0.max(by: shouldNotBeCalled()) }) + } + + // MARK: - TryComparison + + func testTryComparisonBasicBehavior() { + + ComparisonTests.testBasicBehavior( + expectedSubscription: "TryComparison", + expectedResult: 8, + semantics: .max, + { upstream, comparator in upstream.tryMax(by: comparator) } + ) + + ComparisonTests.testBasicBehavior( + expectedSubscription: "TryComparison", + expectedResult: 1, + semantics: .min, + { upstream, comparator in upstream.tryMin(by: comparator) } + ) + } + + func testTryComparisonFailureBecauseOfThrow() throws { + + func comparator(_ lhs: Int, _ rhs: Int) throws -> Bool { + if lhs == 3 { + throw TestingError.oops + } + return lhs < rhs + } + + try ReduceTests.testFailureBecauseOfThrow(expectedSubscription: "TryComparison", + expectedFailure: TestingError.oops, + { $0.tryMax(by: comparator) }) + + try ReduceTests.testFailureBecauseOfThrow(expectedSubscription: "TryComparison", + expectedFailure: TestingError.oops, + { $0.tryMin(by: comparator) }) + } + + func testTryComparisonUpstreamFinishesWithError() { + ReduceTests.testUpstreamFinishesWithError(expectedSubscription: "TryComparison", + { $0.tryMax(by: >) }) + + ReduceTests.testUpstreamFinishesWithError(expectedSubscription: "TryComparison", + { $0.tryMin(by: >) }) + } + + func testTryComparisonUpstreamFinishesImmediately() { + ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "TryComparison", + expectedResult: nil, + { $0.tryMax(by: >) }) + + ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "TryComparison", + expectedResult: nil, + { $0.tryMin(by: >) }) + } + + func testTryComparisonCancelAlreadyCancelled() throws { + try ReduceTests.testCancelAlreadyCancelled { $0.tryMax(by: shouldNotBeCalled()) } + try ReduceTests.testCancelAlreadyCancelled { $0.tryMin(by: shouldNotBeCalled()) } + } + + func testTryComparisonRequestsUnlimitedThenSendsSubscription() { + ReduceTests.testRequestsUnlimitedThenSendsSubscription { + $0.tryMax(by: shouldNotBeCalled()) + } + + ReduceTests.testRequestsUnlimitedThenSendsSubscription { + $0.tryMin(by: shouldNotBeCalled()) + } + } + + func testTryComparisonReceiveSubscriptionTwice() throws { + try ReduceTests.testReceiveSubscriptionTwice( + expectedSubscription: "TryComparison", + expectedResult: .normalCompletion(0), + { $0.tryMax(by: shouldNotBeCalled()) } + ) + + try ReduceTests.testReceiveSubscriptionTwice( + expectedSubscription: "TryComparison", + expectedResult: .normalCompletion(0), + { $0.tryMin(by: shouldNotBeCalled()) } + ) + } + + func testTryComparisonLifecycle() throws { + try testLifecycle(sendValue: 31, + cancellingSubscriptionReleasesSubscriber: false, + { $0.tryMin(by: >) }) + + try testLifecycle(sendValue: 31, + cancellingSubscriptionReleasesSubscriber: false, + { $0.tryMax(by: >) }) + } + + func testTryComparisonReflection() throws { + try testReflection(parentInput: Int.self, + parentFailure: Error.self, + description: "TryComparison", + customMirror: reduceLikeOperatorMirror(), + playgroundDescription: "TryComparison", + { $0.tryMin(by: shouldNotBeCalled()) }) + + try testReflection(parentInput: Int.self, + parentFailure: Error.self, + description: "TryComparison", + customMirror: reduceLikeOperatorMirror(), + playgroundDescription: "TryComparison", + { $0.tryMax(by: shouldNotBeCalled()) }) + } + + // MARK: - Generic tests + + private enum ComparisonSemantics { + case min + case max + } + + private struct ComparisonHistoryElement: Equatable, CustomStringConvertible { + let lhs: Int + let rhs: Int + + init(_ lhs: Int, _ rhs: Int) { + self.lhs = lhs + self.rhs = rhs + } + + var description: String { return "(\(lhs), \(rhs))" } + } + + /// Publishes 2, 1, 4, 6, 15, 8, `.finished`, 7, 32. + /// Uses `Int.trailingZeroBitCount` for comparing values. + /// Therefore, for the passed comparator 8 is max, 1 is min. + private static func testBasicBehavior( + expectedSubscription: StringSubscription, + expectedResult: Int, + semantics: ComparisonSemantics, + countComparatorCalls: Bool = true, + _ makeOperator: (CustomPublisher, @escaping (Int, Int) -> Bool) -> Operator + ) where Operator.Output == Int { + + var comparisonHistory = [ComparisonHistoryElement]() + + func comparator(_ lhs: Int, _ rhs: Int) -> Bool { + comparisonHistory.append(.init(lhs, rhs)) + + // Some custom logic to make sure the publisher doesn't use '<'. + return lhs.trailingZeroBitCount < rhs.trailingZeroBitCount + } + + let helper = OperatorTestHelper( + publisherType: CustomPublisher.self, + initialDemand: .max(1), + receiveValueDemand: .none, + createSut: { makeOperator($0, comparator) } + ) + + XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)]) + XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription)]) + + XCTAssertEqual(helper.publisher.send(2), .none) // trailingZeroBitCount = 1 + XCTAssertEqual(helper.publisher.send(1), .none) // trailingZeroBitCount = 0 + XCTAssertEqual(helper.publisher.send(4), .none) // trailingZeroBitCount = 2 + XCTAssertEqual(helper.publisher.send(6), .none) // trailingZeroBitCount = 1 + XCTAssertEqual(helper.publisher.send(15), .none) // trailingZeroBitCount = 0 + XCTAssertEqual(helper.publisher.send(8), .none) // trailingZeroBitCount = 3 + XCTAssertEqual(helper.publisher.send(12), .none) // trailingZeroBitCount = 2 + + if countComparatorCalls { + switch semantics { + case .max: + XCTAssertEqual(comparisonHistory, [.init(2, 1), + .init(2, 4), + .init(4, 6), + .init(4, 15), + .init(4, 8), + .init(8, 12)]) + case .min: + XCTAssertEqual(comparisonHistory, [.init(1, 2), + .init(4, 1), + .init(6, 1), + .init(15, 1), + .init(8, 1), + .init(12, 1)]) + } + } + + XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)]) + XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription)]) + + helper.publisher.send(completion: .finished) + + XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)]) + XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription), + .value(expectedResult), + .completion(.finished)]) + + XCTAssertEqual(helper.publisher.send(7), .none) // trailingZeroBitCount = 0 + XCTAssertEqual(helper.publisher.send(32), .none) // trailingZeroBitCount = 5 + + XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)]) + XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription), + .value(expectedResult), + .completion(.finished)]) + + if countComparatorCalls { + XCTAssertEqual(comparisonHistory.count, 6) + } + } +} + +private func shouldNotBeCalled( + file: StaticString = #file, line: UInt = #line +) -> (Int, Int) -> Bool { + return { _, _ in + XCTFail("Should not be called", file: file, line: line) + return true + } +}