Files
OpenCombine/Tests/OpenCombineTests/Helpers/TrackingSubscriber.swift

405 lines
13 KiB
Swift

//
// TrackingSubscriber.swift
//
//
// Created by Sergej Jaskiewicz on 11.06.2019.
//
import XCTest
#if OPENCOMBINE_COMPATIBILITY_TEST
import Combine
#else
import OpenCombine
#endif
/// `TrackingSubscriber` records every event like "receiveSubscription",
/// "receiveValue" and "receiveCompletion" into its `history` property,
/// optionally executing the provided callbacks.
///
/// This is useful when testing operators that somehow transform their upstream's output.
///
/// Note that `TrackingSubscriber.Event` is equatable, but doesn't respect
/// the subscription, in other words,
/// `TrackingSubscriber.Event.subscription(Subscription.empty)`
/// is considered equal to any other subscription no matter what the subscription object
/// actually is.
@available(macOS 10.15, iOS 13.0, *)
typealias TrackingSubscriber = TrackingSubscriberBase<Int, TestingError>
/// `TrackingSubscriber` records every event like "receiveSubscription",
/// "receiveValue" and "receiveCompletion" into its `history` property,
/// optionally executing the provided callbacks.
///
/// This is useful when testing operators that somehow transform their upstream's output.
///
/// Note that `TrackingSubscriber.Event` is equatable, but doesn't respect
/// the subscription, in other words,
/// `TrackingSubscriber.Event.subscription(Subscription.empty)`
/// is considered equal to any other subscription no matter what the subscription object
/// actually is.
@available(macOS 10.15, iOS 13.0, *)
final class TrackingSubscriberBase<Value, Failure: Error>
: Subscriber,
Cancellable,
CustomStringConvertible
{
enum Event: CustomStringConvertible {
case subscription(StringSubscription)
case value(Value)
case completion(Subscribers.Completion<Failure>)
var description: String {
switch self {
case .subscription(let subscription):
return ".subscription(\"\(subscription)\")"
case .value(let value):
return ".value(\(value))"
case .completion(.finished):
return ".completion(.finished)"
case .completion(.failure(let error)):
return ".completion(.failure(\(error)))"
}
}
}
private let _receiveSubscription: ((Subscription) -> Void)?
private let _receiveValue: ((Input) -> Subscribers.Demand)?
private let _receiveCompletion: ((Subscribers.Completion<Failure>) -> Void)?
private let _onDeinit: (() -> Void)?
var onSubscribe: ((Subscription) -> Void)?
var onValue: ((Input) -> Void)?
var onFinish: (() -> Void)?
var onFailure: ((Failure) -> Void)?
var onDeinit: (() -> Void)?
/// The history of subscriptions, inputs and completions of this subscriber
private(set) var history: [Event] = []
/// A lazy view on `history` with all events except subscriptions filtered out
var subscriptions: LazyMapSequence<
LazyFilterSequence<LazyMapSequence<[Event], StringSubscription?>>,
StringSubscription
> {
return history.lazy.compactMap {
if case .subscription(let s) = $0 {
return s
} else {
return nil
}
}
}
/// A lazy view on `history` with all events except receiving input filtered out
var inputs: LazyMapSequence<
LazyFilterSequence<LazyMapSequence<[Event], Value?>>, Value
> {
return history.lazy.compactMap {
if case .value(let v) = $0 {
return v
} else {
return nil
}
}
}
/// A lazy view on `history` with all events except completions filtered out
var completions: LazyMapSequence<
LazyFilterSequence<
LazyMapSequence<[Event], Subscribers.Completion<Failure>?>
>,
Subscribers.Completion<Failure>
> {
return history.lazy.compactMap {
if case .completion(let c) = $0 {
return c
} else {
return nil
}
}
}
init(receiveSubscription: ((Subscription) -> Void)? = nil,
receiveValue: ((Input) -> Subscribers.Demand)? = nil,
receiveCompletion: ((Subscribers.Completion<Failure>) -> Void)? = nil,
onDeinit: (() -> Void)? = nil) {
_receiveSubscription = receiveSubscription
_receiveValue = receiveValue
_receiveCompletion = receiveCompletion
_onDeinit = onDeinit
}
func receive(subscription: Subscription) {
history.append(.subscription(.init(subscription)))
onSubscribe?(subscription)
_receiveSubscription?(subscription)
}
func receive(_ input: Value) -> Subscribers.Demand {
history.append(.value(input))
onValue?(input)
return _receiveValue?(input) ?? .none
}
func receive(completion: Subscribers.Completion<Failure>) {
history.append(.completion(completion))
switch completion {
case .failure(let error):
onFailure?(error)
case .finished:
onFinish?()
}
_receiveCompletion?(completion)
}
var description: String {
return "\(type(of: self)): \(history)"
}
func assertHistoryEqual(_ expected: [Event],
valueComparator: (Value, Value) -> Bool,
file: StaticString = #file,
line: UInt = #line) {
let equals = history.count == expected.count &&
zip(history, expected)
.allSatisfy { $0.isEqual(to: $1, valueComparator: valueComparator) }
XCTAssert(equals,
"\(history) is not equal to \(expected)",
file: file,
line: line)
}
func cancel() {
for subscription in subscriptions {
subscription.cancel()
}
history = []
}
deinit {
onDeinit?()
_onDeinit?()
}
}
@available(macOS 10.15, iOS 13.0, *)
extension TrackingSubscriberBase where Value: Equatable {
func assertHistoryEqual(_ expected: [Event],
file: StaticString = #file,
line: UInt = #line) {
assertHistoryEqual(expected, valueComparator: ==, file: file, line: line)
}
}
@available(macOS 10.15, iOS 13.0, *)
extension TrackingSubscriberBase where Value == Void {
func assertHistoryEqual(_ expected: [Event],
file: StaticString = #file,
line: UInt = #line) {
assertHistoryEqual(expected,
valueComparator: { _, _ in true },
file: file,
line: line)
}
}
@available(macOS 10.15, iOS 13.0, *)
extension TrackingSubscriberBase.Event {
func isEqual(to other: TrackingSubscriberBase<Value, Failure>.Event,
valueComparator: (Value, Value) -> Bool) -> Bool {
switch (self, other) {
case let (.subscription(lhs), .subscription(rhs)):
return lhs == rhs
case let (.value(lhs), .value(rhs)):
return valueComparator(lhs, rhs)
case let (.completion(lhs), .completion(rhs)):
switch (lhs, rhs) {
case (.finished, .finished):
return true
case let (.failure(lhs), .failure(rhs)):
return (lhs as? TestingError) == (rhs as? TestingError)
default:
return false
}
default:
return false
}
}
}
@available(macOS 10.15, iOS 13.0, *)
extension TrackingSubscriberBase.Event: Equatable where Value: Equatable {
static func == (lhs: TrackingSubscriberBase.Event,
rhs: TrackingSubscriberBase.Event) -> Bool {
return lhs.isEqual(to: rhs, valueComparator: ==)
}
}
@available(macOS 10.15, iOS 13.0, *)
extension TrackingSubscriberBase.Event where Value == Void {
static var signal: TrackingSubscriberBase.Event { return .value(()) }
static func == (lhs: TrackingSubscriberBase.Event,
rhs: TrackingSubscriberBase.Event) -> Bool {
return lhs.isEqual(to: rhs, valueComparator: { _, _ in true })
}
}
@available(macOS 10.15, iOS 13.0, *)
typealias TrackingSubject<Output: Equatable> = TrackingSubjectBase<Output, TestingError>
@available(macOS 10.15, iOS 13.0, *)
final class TrackingSubjectBase<Output: Equatable, Failure: Error>
: Subject,
CustomStringConvertible
{
enum Event: Equatable, CustomStringConvertible {
case subscriber
case subscription(StringSubscription)
case value(Output)
case completion(Subscribers.Completion<Failure>)
static func == (lhs: Event, rhs: Event) -> Bool {
switch (lhs, rhs) {
case (.subscriber, .subscriber):
return true
case let (.subscription(lhs), .subscription(rhs)):
return lhs == rhs
case let (.value(lhs), .value(rhs)):
return lhs == rhs
case let (.completion(lhs), .completion(rhs)):
switch (lhs, rhs) {
case (.finished, .finished):
return true
case let (.failure(lhs), .failure(rhs)):
return (lhs as? TestingError) == (rhs as? TestingError)
default:
return false
}
default:
return false
}
}
var description: String {
switch self {
case .subscriber:
return ".subscriber"
case .subscription(let description):
return ".subscription(\"\(description)\")"
case .value(let value):
return ".value(\(value))"
case .completion(.finished):
return ".completion(.finished)"
case .completion(.failure(let error)):
return ".completion(.failure(\(error))"
}
}
}
private let _passthrough = PassthroughSubject<Output, Failure>()
private(set) var history: [Event] = []
private let _receiveSubscriber: ((AnySubscriber<Output, Failure>) -> Void)?
private let _onDeinit: (() -> Void)?
init(receiveSubscriber: ((AnySubscriber<Output, Failure>) -> Void)? = nil,
onDeinit: (() -> Void)? = nil) {
_receiveSubscriber = receiveSubscriber
_onDeinit = onDeinit
}
deinit {
_onDeinit?()
}
func send(subscription: Subscription) {
history.append(.subscription(.subscription(subscription)))
_passthrough.send(subscription: subscription)
}
func send(_ value: Output) {
history.append(.value(value))
_passthrough.send(value)
}
func send(completion: Subscribers.Completion<Failure>) {
history.append(.completion(completion))
_passthrough.send(completion: completion)
}
func receive<Downstream: Subscriber>(subscriber: Downstream)
where Failure == Downstream.Failure, Output == Downstream.Input
{
_receiveSubscriber?(AnySubscriber(subscriber))
history.append(.subscriber)
_passthrough.subscribe(subscriber)
}
var description: String { return "TrackingSubject" }
}
@available(macOS 10.15, iOS 13.0, *)
enum StringSubscription: Subscription,
CustomStringConvertible,
ExpressibleByStringLiteral {
case string(String)
case subscription(Subscription)
init(_ subscription: Subscription) {
self = .subscription(subscription)
}
init(stringLiteral value: String) {
self = .string(value)
}
var description: String {
switch self {
case .string(let string):
return string
case .subscription(let subscription):
return String(describing: subscription)
}
}
func request(_ demand: Subscribers.Demand) {
underlying?.request(demand)
}
var combineIdentifier: CombineIdentifier {
switch self {
case .subscription(let subscription):
return subscription.combineIdentifier
case .string:
fatalError("String has no combineIdentifier")
}
}
func cancel() {
underlying?.cancel()
}
var underlying: Subscription? {
switch self {
case .string:
return nil
case .subscription(let underlying):
return underlying
}
}
}
@available(macOS 10.15, iOS 13.0, *)
extension StringSubscription: Equatable {
static func == (lhs: StringSubscription, rhs: StringSubscription) -> Bool {
return lhs.description == rhs.description
}
}