From 46729e14eabe22a33f011748d184651d3301d07d Mon Sep 17 00:00:00 2001 From: jguz-pubnub Date: Wed, 9 Aug 2023 15:46:18 +0200 Subject: [PATCH] PR fixes * Introduced regular invocation * Moving code related with EffectInvocation into one place * Moving Equatable conformance for Events & Invocations into test target * Ensuring that invalid transitions are not processed * Fixing the Transition function for Presence * Using built-in methods like union and substracting --- PubNub.xcodeproj/project.pbxproj | 12 ++ .../PubNub/EventEngine/Core/Dispatcher.swift | 34 +++-- .../PubNub/EventEngine/Core/EventEngine.swift | 23 ++- .../EventEngine/Core/TransitionProtocol.swift | 27 ++-- .../Presence/Helpers/PresenceInput.swift | 31 ++-- .../EventEngine/Presence/Presence.swift | 25 +-- .../Presence/PresenceTransition.swift | 22 +-- .../Subscribe/Helpers/SubscribeInput.swift | 17 ++- .../EventEngine/Subscribe/Subscribe.swift | 22 --- .../Subscribe/SubscribeTransition.swift | 2 +- .../Steps/PubNubEventEngineTestsHelpers.swift | 8 +- ...NubSubscribeEngineContractTestsSteps.swift | 144 +++++++++--------- .../Helpers/EffectInvocation+Equatable.swift | 45 ++++++ .../Presence/PresenceTransitionTests.swift | 51 +++++-- .../Subscribe/SubscribeTransitionTests.swift | 27 ++++ 15 files changed, 289 insertions(+), 201 deletions(-) create mode 100644 Tests/PubNubTests/EventEngine/Helpers/EffectInvocation+Equatable.swift diff --git a/PubNub.xcodeproj/project.pbxproj b/PubNub.xcodeproj/project.pbxproj index d439f935..d2ecb2f8 100644 --- a/PubNub.xcodeproj/project.pbxproj +++ b/PubNub.xcodeproj/project.pbxproj @@ -396,6 +396,7 @@ 3D9B29FA2A65609000C988C9 /* EmitMessagesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D9B29F42A65609000C988C9 /* EmitMessagesTests.swift */; }; 3D9B29FB2A65609000C988C9 /* SubscribeRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D9B29F52A65609000C988C9 /* SubscribeRequestTests.swift */; }; 3D9B29FD2A6560FB00C988C9 /* PresenceTransitionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D9B29FC2A6560FB00C988C9 /* PresenceTransitionTests.swift */; }; + 3DB0E9102A83E0110065B4FD /* EffectInvocation+Equatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DB0E90F2A83E0110065B4FD /* EffectInvocation+Equatable.swift */; }; 3DB56B622A715CB100FC35A0 /* PresenceEffectFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DB56B612A715CB100FC35A0 /* PresenceEffectFactory.swift */; }; 3DB56B652A715F7E00FC35A0 /* HeartbeatEffectTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DB56B632A715F1700FC35A0 /* HeartbeatEffectTests.swift */; }; 3DCC4DE02A42E93200F4A67A /* subscription_handshake_success.json in Resources */ = {isa = PBXBuildFile; fileRef = 3DCC4DDF2A42E93200F4A67A /* subscription_handshake_success.json */; }; @@ -952,6 +953,7 @@ 3D9B29F42A65609000C988C9 /* EmitMessagesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmitMessagesTests.swift; sourceTree = ""; }; 3D9B29F52A65609000C988C9 /* SubscribeRequestTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscribeRequestTests.swift; sourceTree = ""; }; 3D9B29FC2A6560FB00C988C9 /* PresenceTransitionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresenceTransitionTests.swift; sourceTree = ""; }; + 3DB0E90F2A83E0110065B4FD /* EffectInvocation+Equatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EffectInvocation+Equatable.swift"; sourceTree = ""; }; 3DB56B612A715CB100FC35A0 /* PresenceEffectFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresenceEffectFactory.swift; sourceTree = ""; }; 3DB56B632A715F1700FC35A0 /* HeartbeatEffectTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeartbeatEffectTests.swift; sourceTree = ""; }; 3DCC4DDF2A42E93200F4A67A /* subscription_handshake_success.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = subscription_handshake_success.json; sourceTree = ""; }; @@ -2022,6 +2024,14 @@ path = Subscribe; sourceTree = ""; }; + 3DB0E90E2A83E0010065B4FD /* Helpers */ = { + isa = PBXGroup; + children = ( + 3DB0E90F2A83E0110065B4FD /* EffectInvocation+Equatable.swift */, + ); + path = Helpers; + sourceTree = ""; + }; 3DBD7CDD58292DFFDF108B95 /* Pods */ = { isa = PBXGroup; children = ( @@ -2083,6 +2093,7 @@ 3DE748892A1FA449009B0809 /* DispatcherTests.swift */, 3D9B29EE2A65605900C988C9 /* Presence */, 3D9B29EF2A65605900C988C9 /* Subscribe */, + 3DB0E90E2A83E0010065B4FD /* Helpers */, ); path = EventEngine; sourceTree = ""; @@ -3330,6 +3341,7 @@ files = ( 3558069C231303D9005CDD92 /* AutomaticRetryTests.swift in Sources */, 35D973542857BBFE001A44DC /* FlatJSONCodable+Test.swift in Sources */, + 3DB0E9102A83E0110065B4FD /* EffectInvocation+Equatable.swift in Sources */, 35FE93B922EE44F70051C455 /* MockURLSession.swift in Sources */, 3D9B29FD2A6560FB00C988C9 /* PresenceTransitionTests.swift in Sources */, 3D9B29F82A65609000C988C9 /* SubscribeInputTests.swift in Sources */, diff --git a/Sources/PubNub/EventEngine/Core/Dispatcher.swift b/Sources/PubNub/EventEngine/Core/Dispatcher.swift index 1a1562ee..a0439acd 100644 --- a/Sources/PubNub/EventEngine/Core/Dispatcher.swift +++ b/Sources/PubNub/EventEngine/Core/Dispatcher.swift @@ -66,21 +66,35 @@ class EffectDispatcher: Dispatche with customInput: EventEngineCustomInput, notify listener: DispatcherListener ) { - let effectsToRun = invocations.compactMap { + invocations.forEach { switch $0 { case .managed(let invocation): - return EffectWrapper(id: invocation.id, effect: factory.effect(for: invocation, with: customInput)) + executeEffect( + effect: factory.effect(for: invocation, with: customInput), + storageId: invocation.id, + notify: listener + ) + case .regular(let invocation): + executeEffect( + effect: factory.effect(for: invocation, with: customInput), + storageId: UUID().uuidString, + notify: listener + ) case .cancel(let cancelInvocation): - effectsCache.getEffect(with: cancelInvocation.id)?.cancelTask(); return nil + effectsCache.getEffect(with: cancelInvocation.id)?.cancelTask() } } - - effectsToRun.forEach { - effectsCache.put(effect: $0.effect, with: $0.id) - $0.effect.performTask { [weak effectsCache, effectId = $0.id] results in - listener.onAnyInvocationCompleted(results) - effectsCache?.removeEffect(id: effectId) - } + } + + private func executeEffect( + effect: some EffectHandler, + storageId id: String, + notify listener: DispatcherListener + ) { + effectsCache.put(effect: effect, with: id) + effect.performTask { [weak effectsCache] results in + listener.onAnyInvocationCompleted(results) + effectsCache?.removeEffect(id: id) } } } diff --git a/Sources/PubNub/EventEngine/Core/EventEngine.swift b/Sources/PubNub/EventEngine/Core/EventEngine.swift index 8eff1cae..8b81b430 100644 --- a/Sources/PubNub/EventEngine/Core/EventEngine.swift +++ b/Sources/PubNub/EventEngine/Core/EventEngine.swift @@ -27,18 +27,6 @@ import Foundation -protocol AnyIdentifiableInvocation { - var id: String { get } -} - -protocol AnyCancellableInvocation: AnyIdentifiableInvocation, Equatable { - -} - -protocol AnyEffectInvocation: AnyIdentifiableInvocation, Equatable { - associatedtype Cancellable: AnyCancellableInvocation -} - protocol EventEngineDelegate: AnyObject { associatedtype State func onStateUpdated(state: State) @@ -71,7 +59,16 @@ class EventEngine { func send(event: Event) { objc_sync_enter(self) - defer { objc_sync_exit(self) } + + defer { + objc_sync_exit(self) + } + guard transition.canTransition( + from: state, + dueTo: event + ) else { + return + } let transitionResult = transition.transition(from: state, event: event) let invocations = transitionResult.invocations diff --git a/Sources/PubNub/EventEngine/Core/TransitionProtocol.swift b/Sources/PubNub/EventEngine/Core/TransitionProtocol.swift index 24b537f7..769bb71d 100644 --- a/Sources/PubNub/EventEngine/Core/TransitionProtocol.swift +++ b/Sources/PubNub/EventEngine/Core/TransitionProtocol.swift @@ -27,6 +27,18 @@ import Foundation +protocol AnyIdentifiableInvocation { + var id: String { get } +} + +protocol AnyCancellableInvocation: AnyIdentifiableInvocation { + +} + +protocol AnyEffectInvocation: AnyIdentifiableInvocation { + associatedtype Cancellable: AnyCancellableInvocation +} + struct TransitionResult { let state: State let invocations: [EffectInvocation] @@ -37,20 +49,10 @@ struct TransitionResult { } } -enum EffectInvocation: Equatable { +enum EffectInvocation { case managed(_ invocation: Invocation) + case regular(_ invocation: Invocation) case cancel(_ invocation: Invocation.Cancellable) - - static func == (lhs: EffectInvocation, rhs: EffectInvocation) -> Bool { - switch (lhs, rhs) { - case (let .managed(lhsInvocation), let .managed(rhsInvocation)): - return lhsInvocation == rhsInvocation - case (let .cancel(lhsId), let .cancel(rhsId)): - return lhsId.id == rhsId.id - default: - return false - } - } } protocol TransitionProtocol { @@ -58,5 +60,6 @@ protocol TransitionProtocol { associatedtype Event associatedtype Invocation: AnyEffectInvocation + func canTransition(from state: State, dueTo event: Event) -> Bool func transition(from state: State, event: Event) -> TransitionResult } diff --git a/Sources/PubNub/EventEngine/Presence/Helpers/PresenceInput.swift b/Sources/PubNub/EventEngine/Presence/Helpers/PresenceInput.swift index ab22dc6a..f3f9a951 100644 --- a/Sources/PubNub/EventEngine/Presence/Helpers/PresenceInput.swift +++ b/Sources/PubNub/EventEngine/Presence/Helpers/PresenceInput.swift @@ -32,11 +32,11 @@ struct PresenceInput: Equatable { fileprivate let groupsSet: Set var channels: [String] { - channelsSet.allObjects.sorted(by: <) + channelsSet.map { $0 } } var groups: [String] { - groupsSet.allObjects.sorted(by: <) + groupsSet.map { $0 } } var isEmpty: Bool { @@ -54,26 +54,23 @@ struct PresenceInput: Equatable { } static func +(lhs: PresenceInput, rhs: PresenceInput) -> PresenceInput { - var uniqueChannels = lhs.channelsSet - var uniqueGroups = lhs.groupsSet - - rhs.channelsSet.forEach { uniqueChannels.insert($0) } - rhs.groupsSet.forEach { uniqueGroups.insert($0) } - - return PresenceInput(channels: uniqueChannels, groups: uniqueGroups) + PresenceInput( + channels: lhs.channelsSet.union(rhs.channelsSet), + groups: lhs.groupsSet.union(rhs.groupsSet) + ) } static func -(lhs: PresenceInput, rhs: PresenceInput) -> PresenceInput { - var uniqueChannels = lhs.channelsSet - var uniqueGroups = lhs.groupsSet - - rhs.channelsSet.forEach { uniqueChannels.remove($0) } - rhs.groupsSet.forEach { uniqueGroups.remove($0) } - - return PresenceInput(channels: uniqueChannels, groups: uniqueGroups) + PresenceInput( + channels: lhs.channelsSet.subtracting(rhs.channelsSet), + groups: lhs.groupsSet.subtracting(rhs.groupsSet) + ) } static func ==(lhs: PresenceInput, rhs: PresenceInput) -> Bool { - lhs.channels == rhs.channels && lhs.groups == rhs.groups + let equalChannels = lhs.channels.sorted(by: <) == rhs.channels.sorted(by: <) + let equalGroups = lhs.groups.sorted(by: <) == rhs.groups.sorted(by: <) + + return equalChannels && equalGroups } } diff --git a/Sources/PubNub/EventEngine/Presence/Presence.swift b/Sources/PubNub/EventEngine/Presence/Presence.swift index beb35921..46791110 100644 --- a/Sources/PubNub/EventEngine/Presence/Presence.swift +++ b/Sources/PubNub/EventEngine/Presence/Presence.swift @@ -40,10 +40,6 @@ extension PresenceState { var groups: [String] { input.groups } - - func isEqual(to otherState: some PresenceState) -> Bool { - (otherState as? Self) == self - } } // @@ -111,23 +107,8 @@ extension Presence { case heartbeat(channels: [String], groups: [String]) case leave(channels: [String], groups: [String]) case delayedHeartbeat(channels: [String], groups: [String], currentAttempt: Int, error: PubNubError) - case scheduleNextHeartbeat(channels: [String], groups: [String]) - - public static func ==(lhs: Presence.Invocation, rhs: Presence.Invocation) -> Bool { - switch (lhs, rhs) { - case let (.heartbeat(lC, lG), .heartbeat(rC, rG)): - return lC == rC && lG == rG - case let (.leave(lC, lG), .leave(rC, rG)): - return lC == rC && lG == rG - case let (.delayedHeartbeat(lC, lG, lAtt, lErr),.delayedHeartbeat(rC, rG, rAtt, rErr)): - return lC == rC && lG == rG && lAtt == rAtt && lErr == rErr - case let (.scheduleNextHeartbeat(lC, lG), .scheduleNextHeartbeat(rC, rG)): - return lC == rC && lG == rG - default: - return false - } - } - + case wait(channels: [String], groups: [String]) + enum Cancellable: AnyCancellableInvocation { case scheduleNextHeartbeat case delayedHeartbeat @@ -146,7 +127,7 @@ extension Presence { switch self { case .heartbeat(_,_): return "Presence.Heartbeat" - case .scheduleNextHeartbeat: + case .wait(_,_): return Cancellable.scheduleNextHeartbeat.id case .delayedHeartbeat: return Cancellable.delayedHeartbeat.id diff --git a/Sources/PubNub/EventEngine/Presence/PresenceTransition.swift b/Sources/PubNub/EventEngine/Presence/PresenceTransition.swift index d64c5197..622fdc86 100644 --- a/Sources/PubNub/EventEngine/Presence/PresenceTransition.swift +++ b/Sources/PubNub/EventEngine/Presence/PresenceTransition.swift @@ -32,12 +32,12 @@ class PresenceTransition: TransitionProtocol { typealias Event = Presence.Event typealias Invocation = Presence.Invocation - private func canTransition(from state: State, dueTo event: Event) -> Bool { + func canTransition(from state: State, dueTo event: Event) -> Bool { switch event { case .joined(_,_): return true case .left(_,_): - return true + return !(state is Presence.HeartbeatInactive) case .heartbeatSuccess: return state is Presence.Heartbeating case .heartbeatFailed(_): @@ -47,7 +47,7 @@ class PresenceTransition: TransitionProtocol { case .timesUp: return state is Presence.HeartbeatCooldown case .leftAll: - return true + return !(state is Presence.HeartbeatInactive) case .disconnect: return true case .reconnect: @@ -55,12 +55,12 @@ class PresenceTransition: TransitionProtocol { } } - func onEntry(to state: State) -> [EffectInvocation] { + private func onEntry(to state: State) -> [EffectInvocation] { switch state { case is Presence.Heartbeating: - return [.managed(.heartbeat(channels: state.channels, groups: state.input.groups))] + return [.regular(.heartbeat(channels: state.channels, groups: state.input.groups))] case is Presence.HeartbeatCooldown: - return [.managed(.scheduleNextHeartbeat(channels: state.channels, groups: state.groups))] + return [.managed(.wait(channels: state.channels, groups: state.groups))] case let state as Presence.HeartbeatReconnecting: return [.managed(.delayedHeartbeat(channels: state.channels, groups: state.groups, currentAttempt: state.currentAttempt, error: state.error))] default: @@ -68,7 +68,7 @@ class PresenceTransition: TransitionProtocol { } } - func onExit(from state: State) -> [EffectInvocation] { + private func onExit(from state: State) -> [EffectInvocation] { switch state { case is Presence.HeartbeatCooldown: return [.cancel(.scheduleNextHeartbeat)] @@ -149,12 +149,12 @@ fileprivate extension PresenceTransition { } else if newInput.isEmpty { return TransitionResult( state: Presence.HeartbeatInactive(), - invocations: [.managed(.leave(channels: channels, groups: groups))] + invocations: [.regular(.leave(channels: channels, groups: groups))] ) } else { return TransitionResult( state: Presence.Heartbeating(input: newInput), - invocations: [.managed(.leave(channels: channels, groups: groups))] + invocations: [.regular(.leave(channels: channels, groups: groups))] ) } } @@ -205,7 +205,7 @@ fileprivate extension PresenceTransition { func heartbeatStoppedTransition(from state: State) -> TransitionResult { return TransitionResult( state: Presence.HeartbeatStopped(input: state.input), - invocations: [.managed(.leave(channels: state.input.channels, groups: state.input.groups))] + invocations: [.regular(.leave(channels: state.input.channels, groups: state.input.groups))] ) } } @@ -214,7 +214,7 @@ fileprivate extension PresenceTransition { func heartbeatInactiveTransition(from state: State) -> TransitionResult { return TransitionResult( state: Presence.HeartbeatInactive(), - invocations: [.managed(.leave(channels: state.input.channels, groups: state.input.groups))] + invocations: [.regular(.leave(channels: state.input.channels, groups: state.input.groups))] ) } } diff --git a/Sources/PubNub/EventEngine/Subscribe/Helpers/SubscribeInput.swift b/Sources/PubNub/EventEngine/Subscribe/Helpers/SubscribeInput.swift index 51c490dd..4f97d2da 100644 --- a/Sources/PubNub/EventEngine/Subscribe/Helpers/SubscribeInput.swift +++ b/Sources/PubNub/EventEngine/Subscribe/Helpers/SubscribeInput.swift @@ -58,11 +58,11 @@ struct SubscribeInput: Equatable { } var subscribedChannels: [String] { - channels.map { $0.key }.sorted(by: <) + channels.map { $0.key } } var subscribedGroups: [String] { - groups.map { $0.key }.sorted(by: <) + groups.map { $0.key } } var allSubscribedChannels: [String] { @@ -71,7 +71,7 @@ struct SubscribeInput: Equatable { if entry.value.isPresenceSubscribed { result.append(entry.value.presenceId) } - }.sorted(by: <) + } } var allSubscribedGroups: [String] { @@ -80,7 +80,7 @@ struct SubscribeInput: Equatable { if entry.value.isPresenceSubscribed { result.append(entry.value.presenceId) } - }.sorted(by: <) + } } var totalSubscribedCount: Int { @@ -126,6 +126,13 @@ struct SubscribeInput: Equatable { filterExpression: self.filterExpression ) } + + static func ==(lhs: SubscribeInput, rhs: SubscribeInput) -> Bool { + let equalChannels = lhs.allSubscribedChannels.sorted(by: <) == rhs.allSubscribedChannels.sorted(by: <) + let equalGroups = lhs.allSubscribedGroups.sorted(by: <) == rhs.allSubscribedGroups.sorted(by: <) + + return equalChannels && equalGroups + } } extension Dictionary where Key == String, Value == PubNubChannel { @@ -140,7 +147,7 @@ extension Dictionary where Key == String, Value == PubNubChannel { // Updates current Dictionary with the new channel value unsubscribed from Presence. // Returns the updated value if the corresponding entry matching the passed `id:` was found, otherwise `nil` - mutating func unsubscribePresence(_ id: String) -> Value? { + @discardableResult mutating func unsubscribePresence(_ id: String) -> Value? { if let match = self[id], match.isPresenceSubscribed { let updatedChannel = PubNubChannel(id: match.id, withPresence: false) self[match.id] = updatedChannel diff --git a/Sources/PubNub/EventEngine/Subscribe/Subscribe.swift b/Sources/PubNub/EventEngine/Subscribe/Subscribe.swift index b72ff21b..deaf2a3b 100644 --- a/Sources/PubNub/EventEngine/Subscribe/Subscribe.swift +++ b/Sources/PubNub/EventEngine/Subscribe/Subscribe.swift @@ -39,9 +39,6 @@ extension SubscribeState { var hasTimetoken: Bool { return cursor.timetoken != 0 } - func isEqual(to otherState: some SubscribeState) -> Bool { - (otherState as? Self) == self - } } // @@ -185,25 +182,6 @@ extension Subscribe { } } } - - public static func ==(lhs: Subscribe.Invocation, rhs: Subscribe.Invocation) -> Bool { - switch (lhs, rhs) { - case let (.handshakeRequest(lC, lG), .handshakeRequest(rC, rG)): - return lC == rC && lG == rG - case let (.handshakeReconnect(lC, lG, lAtt, lErr),.handshakeReconnect(rC, rG, rAtt, rErr)): - return lC == rC && lG == rG && lAtt == rAtt && lErr == rErr - case let (.receiveMessages(lC, lG, lCrsr),.receiveMessages(rC, rG, rCrsr)): - return lC == rC && lG == rG && lCrsr == rCrsr - case let (.receiveReconnect(lC, lG, lCrsr, lAtt, lErr), .receiveReconnect(rC, rG, rCrsr, rAtt, rErr)): - return lC == rC && lG == rG && lCrsr == rCrsr && lAtt == rAtt && lErr == rErr - case let (.emitStatus(lhsChange), .emitStatus(rhsChange)): - return lhsChange == rhsChange - case let (.emitMessages(lhsMssgs, lhsCrsr), .emitMessages(rhsMssgs, rhsCrsr)): - return lhsMssgs == rhsMssgs && lhsCrsr == rhsCrsr - default: - return false - } - } var id: String { switch self { diff --git a/Sources/PubNub/EventEngine/Subscribe/SubscribeTransition.swift b/Sources/PubNub/EventEngine/Subscribe/SubscribeTransition.swift index 6b0ff658..2f5eda41 100644 --- a/Sources/PubNub/EventEngine/Subscribe/SubscribeTransition.swift +++ b/Sources/PubNub/EventEngine/Subscribe/SubscribeTransition.swift @@ -32,7 +32,7 @@ class SubscribeTransition: TransitionProtocol { typealias Event = Subscribe.Event typealias Invocation = Subscribe.Invocation - private func canTransition(from state: State, dueTo event: Event) -> Bool { + func canTransition(from state: State, dueTo event: Event) -> Bool { switch event { case .handshakeSucceess(_): return state is Subscribe.HandshakingState diff --git a/Tests/PubNubContractTest/Steps/PubNubEventEngineTestsHelpers.swift b/Tests/PubNubContractTest/Steps/PubNubEventEngineTestsHelpers.swift index 7eb8a1d0..c8f869ee 100644 --- a/Tests/PubNubContractTest/Steps/PubNubEventEngineTestsHelpers.swift +++ b/Tests/PubNubContractTest/Steps/PubNubEventEngineTestsHelpers.swift @@ -33,13 +33,15 @@ protocol ContractTestIdentifiable { var contractTestIdentifier: String { get } } -extension EffectInvocation: ContractTestIdentifiable { +extension EffectInvocation: ContractTestIdentifiable where Invocation: ContractTestIdentifiable, Invocation.Cancellable: ContractTestIdentifiable { var contractTestIdentifier: String { switch self { case .managed(let invocation): - return (invocation as? ContractTestIdentifiable)?.contractTestIdentifier ?? "" + return invocation.contractTestIdentifier case .cancel(let cancellable): - return (cancellable as? ContractTestIdentifiable)?.contractTestIdentifier ?? "" + return cancellable.contractTestIdentifier + case .regular(let invocation): + return invocation.contractTestIdentifier } } } diff --git a/Tests/PubNubContractTest/Steps/Subscribe/PubNubSubscribeEngineContractTestsSteps.swift b/Tests/PubNubContractTest/Steps/Subscribe/PubNubSubscribeEngineContractTestsSteps.swift index f9b95d34..13abebad 100644 --- a/Tests/PubNubContractTest/Steps/Subscribe/PubNubSubscribeEngineContractTestsSteps.swift +++ b/Tests/PubNubContractTest/Steps/Subscribe/PubNubSubscribeEngineContractTestsSteps.swift @@ -30,6 +30,77 @@ import Cucumberish @testable import PubNub +extension Subscribe.Invocation: ContractTestIdentifiable { + var contractTestIdentifier: String { + switch self { + case .handshakeRequest(_, _): + return "HANDSHAKE" + case .handshakeReconnect(_, _, _, _): + return "HANDSHAKE_RECONNECT" + case .receiveMessages(_, _, _): + return "RECEIVE_MESSAGES" + case .receiveReconnect(_, _, _, _, _): + return "RECEIVE_RECONNECT" + case .emitMessages(_,_): + return "EMIT_MESSAGES" + case .emitStatus(_): + return "EMIT_STATUS" + } + } +} + +extension Subscribe.Invocation.Cancellable: ContractTestIdentifiable { + var contractTestIdentifier: String { + switch self { + case .handshakeRequest: + return "CANCEL_HANDSHAKE" + case .handshakeReconnect: + return "CANCEL_HANDSHAKE_RECONNECT" + case .receiveMessages: + return "CANCEL_RECEIVE_MESSAGES" + case .receiveReconnect: + return "CANCEL_RECEIVE_RECONNECT" + } + } +} + +extension Subscribe.Event: ContractTestIdentifiable { + var contractTestIdentifier: String { + switch self { + case .handshakeSucceess(_): + return "HANDSHAKE_SUCCESS" + case .handshakeFailure(_): + return "HANDSHAKE_FAILURE" + case .handshakeReconnectSuccess(_): + return "HANDSHAKE_RECONNECT_SUCCESS" + case .handshakeReconnectFailure(_): + return "HANDSHAKE_RECONNECT_FAILURE" + case .handshakeReconnectGiveUp(_): + return "HANDSHAKE_RECONNECT_GIVEUP" + case .receiveSuccess(_,_): + return "RECEIVE_SUCCESS" + case .receiveFailure(_): + return "RECEIVE_FAILURE" + case .receiveReconnectSuccess(_,_): + return "RECEIVE_RECONNECT_SUCCESS" + case .receiveReconnectFailure(_): + return "RECEIVE_RECONNECT_FAILURE" + case .receiveReconnectGiveUp(_): + return "RECEIVE_RECONNECT_GIVEUP" + case .subscriptionChanged(_, _): + return "SUBSCRIPTION_CHANGED" + case .subscriptionRestored(_, _, _): + return "SUBSCRIPTION_RESTORED" + case .unsubscribeAll: + return "UNSUBSCRIBE_ALL" + case .disconnect: + return "DISCONNECT" + case .reconnect: + return "RECONNECT" + } + } +} + class PubNubSubscribeEngineContractTestsSteps: PubNubContractTestCase { // A subscription session with wrapped Disptacher and Transition in order to record Invocations and Events private var subscriptionSession: SubscriptionSession! @@ -47,6 +118,7 @@ class PubNubSubscribeEngineContractTestsSteps: PubNubContractTestCase { subscriptionSession = nil super.handleAfterHook() } + override func handleBeforeHook() { dispatcher = DispatcherDecorator(wrappedInstance: EffectDispatcher( factory: SubscribeEffectFactory(session: HTTPSession( @@ -64,6 +136,7 @@ class PubNubSubscribeEngineContractTestsSteps: PubNubContractTestCase { override var expectSubscribeFailure: Bool { hasStep(with: "I receive an error in my subscribe response") } + override func createPubNubClient() -> PubNub { PubNub(configuration: self.configuration, subscriptionSession: subscriptionSession) } @@ -128,74 +201,3 @@ class PubNubSubscribeEngineContractTestsSteps: PubNubContractTestCase { return (events: events, invocations: invocations) } } - -extension Subscribe.Invocation: ContractTestIdentifiable { - var contractTestIdentifier: String { - switch self { - case .handshakeRequest(_, _): - return "HANDSHAKE" - case .handshakeReconnect(_, _, _, _): - return "HANDSHAKE_RECONNECT" - case .receiveMessages(_, _, _): - return "RECEIVE_MESSAGES" - case .receiveReconnect(_, _, _, _, _): - return "RECEIVE_RECONNECT" - case .emitMessages(_,_): - return "EMIT_MESSAGES" - case .emitStatus(_): - return "EMIT_STATUS" - } - } -} - -extension Subscribe.Invocation.Cancellable: ContractTestIdentifiable { - var contractTestIdentifier: String { - switch self { - case .handshakeRequest: - return "CANCEL_HANDSHAKE" - case .handshakeReconnect: - return "CANCEL_HANDSHAKE_RECONNECT" - case .receiveMessages: - return "CANCEL_RECEIVE_MESSAGES" - case .receiveReconnect: - return "CANCEL_RECEIVE_RECONNECT" - } - } -} - -extension Subscribe.Event: ContractTestIdentifiable { - var contractTestIdentifier: String { - switch self { - case .handshakeSucceess(_): - return "HANDSHAKE_SUCCESS" - case .handshakeFailure(_): - return "HANDSHAKE_FAILURE" - case .handshakeReconnectSuccess(_): - return "HANDSHAKE_RECONNECT_SUCCESS" - case .handshakeReconnectFailure(_): - return "HANDSHAKE_RECONNECT_FAILURE" - case .handshakeReconnectGiveUp(_): - return "HANDSHAKE_RECONNECT_GIVEUP" - case .receiveSuccess(_,_): - return "RECEIVE_SUCCESS" - case .receiveFailure(_): - return "RECEIVE_FAILURE" - case .receiveReconnectSuccess(_,_): - return "RECEIVE_RECONNECT_SUCCESS" - case .receiveReconnectFailure(_): - return "RECEIVE_RECONNECT_FAILURE" - case .receiveReconnectGiveUp(_): - return "RECEIVE_RECONNECT_GIVEUP" - case .subscriptionChanged(_, _): - return "SUBSCRIPTION_CHANGED" - case .subscriptionRestored(_, _, _): - return "SUBSCRIPTION_RESTORED" - case .unsubscribeAll: - return "UNSUBSCRIBE_ALL" - case .disconnect: - return "DISCONNECT" - case .reconnect: - return "RECONNECT" - } - } -} diff --git a/Tests/PubNubTests/EventEngine/Helpers/EffectInvocation+Equatable.swift b/Tests/PubNubTests/EventEngine/Helpers/EffectInvocation+Equatable.swift new file mode 100644 index 00000000..bb8789fe --- /dev/null +++ b/Tests/PubNubTests/EventEngine/Helpers/EffectInvocation+Equatable.swift @@ -0,0 +1,45 @@ +// +// EffectInvocation+Equatable.swift +// +// PubNub Real-time Cloud-Hosted Push API and Push Notification Client Frameworks +// Copyright © 2023 PubNub Inc. +// https://www.pubnub.com/ +// https://www.pubnub.com/terms +// +// 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 Foundation + +@testable import PubNub + +extension EffectInvocation: Equatable where Invocation: Equatable { + public static func ==(lhs: EffectInvocation, rhs: EffectInvocation) -> Bool { + switch (lhs, rhs) { + case (let .managed(lhsInvocation), let .managed(rhsInvocation)): + return lhsInvocation == rhsInvocation + case (let .regular(lhsInvocation), let .regular(rhsInvocation)): + return lhsInvocation == rhsInvocation + case (let .cancel(lhsId), let .cancel(rhsId)): + return lhsId.id == rhsId.id + default: + return false + } + } +} diff --git a/Tests/PubNubTests/EventEngine/Presence/PresenceTransitionTests.swift b/Tests/PubNubTests/EventEngine/Presence/PresenceTransitionTests.swift index 0068c06e..7d3aced7 100644 --- a/Tests/PubNubTests/EventEngine/Presence/PresenceTransitionTests.swift +++ b/Tests/PubNubTests/EventEngine/Presence/PresenceTransitionTests.swift @@ -30,6 +30,29 @@ import XCTest @testable import PubNub +extension Presence.Invocation: Equatable { + public static func ==(lhs: Presence.Invocation, rhs: Presence.Invocation) -> Bool { + switch (lhs, rhs) { + case let (.heartbeat(lC, lG), .heartbeat(rC, rG)): + return lC.sorted(by: <) == rC.sorted(by: <) && lG.sorted(by: <) == rG.sorted(by: <) + case let (.leave(lC, lG), .leave(rC, rG)): + return lC.sorted(by: <) == rC.sorted(by: <) && lG.sorted(by: <) == rG.sorted(by: <) + case let (.delayedHeartbeat(lC, lG, lAtt, lErr),.delayedHeartbeat(rC, rG, rAtt, rErr)): + return lC.sorted(by: <) == rC.sorted(by: <) && lG.sorted(by: <) == rG.sorted(by: <) && lAtt == rAtt && lErr == rErr + case let (.wait(lC, lG), .wait(rC, rG)): + return lC.sorted(by: <) == rC.sorted(by: <) && lG.sorted(by: <) == rG.sorted(by: <) + default: + return false + } + } +} + +extension PresenceState { + func isEqual(to otherState: some PresenceState) -> Bool { + (otherState as? Self) == self + } +} + class PresenceTransitionTests: XCTestCase { private let transition = PresenceTransition() @@ -45,7 +68,7 @@ class PresenceTransitionTests: XCTestCase { event: .joined(channels: ["c3"], groups: ["g3"]) ) let expectedInvocations: [EffectInvocation] = [ - .managed(.heartbeat(channels: ["c1", "c2", "c3"], groups: ["g1", "g2", "g3"])) + .regular(.heartbeat(channels: ["c1", "c2", "c3"], groups: ["g1", "g2", "g3"])) ] let expectedState = Presence.Heartbeating( input: PresenceInput(channels: ["c1", "c2", "c3"], groups: ["g1", "g2", "g3"]) @@ -83,7 +106,7 @@ class PresenceTransitionTests: XCTestCase { ) let expectedInvocations: [EffectInvocation] = [ .cancel(.delayedHeartbeat), - .managed(.heartbeat(channels: ["c1", "c2", "c3"], groups: ["g1", "g2", "g3"])) + .regular(.heartbeat(channels: ["c1", "c2", "c3"], groups: ["g1", "g2", "g3"])) ] let expectedState = Presence.Heartbeating( input: PresenceInput(channels: ["c1", "c2", "c3"], groups: ["g1", "g2", "g3"]) @@ -105,8 +128,8 @@ class PresenceTransitionTests: XCTestCase { event: .left(channels: ["c3"], groups: ["g3"]) ) let expectedInvocations: [EffectInvocation] = [ - .managed(.leave(channels: ["c3"], groups: ["g3"])), - .managed(.heartbeat(channels: ["c1", "c2"], groups: ["g1", "g2"])) + .regular(.leave(channels: ["c3"], groups: ["g3"])), + .regular(.heartbeat(channels: ["c1", "c2"], groups: ["g1", "g2"])) ] let expectedState = Presence.Heartbeating( input: PresenceInput(channels: ["c1", "c2"], groups: ["g1", "g2"]) @@ -144,8 +167,8 @@ class PresenceTransitionTests: XCTestCase { ) let expectedInvocations: [EffectInvocation] = [ .cancel(.delayedHeartbeat), - .managed(.leave(channels: ["c3"], groups: ["g3"])), - .managed(.heartbeat(channels: ["c1", "c2"], groups: ["g1", "g2"])) + .regular(.leave(channels: ["c3"], groups: ["g3"])), + .regular(.heartbeat(channels: ["c1", "c2"], groups: ["g1", "g2"])) ] let expectedState = Presence.Heartbeating( input: PresenceInput(channels: ["c1", "c2"], groups: ["g1", "g2"]) @@ -166,7 +189,7 @@ class PresenceTransitionTests: XCTestCase { ) let expectedInvocations: [EffectInvocation] = [ .cancel(.scheduleNextHeartbeat), - .managed(.leave(channels: ["c1", "c2", "c3"], groups: ["g1", "g2", "g3"])), + .regular(.leave(channels: ["c1", "c2", "c3"], groups: ["g1", "g2", "g3"])), ] let expectedState = Presence.HeartbeatInactive() @@ -186,7 +209,7 @@ class PresenceTransitionTests: XCTestCase { event: .leftAll ) let expectedInvocations: [EffectInvocation] = [ - .managed(.leave(channels: ["c1", "c2"], groups: ["g1", "g2"])) + .regular(.leave(channels: ["c1", "c2"], groups: ["g1", "g2"])) ] let expectedState = Presence.HeartbeatInactive() @@ -206,7 +229,7 @@ class PresenceTransitionTests: XCTestCase { event: .reconnect ) let expectedInvocations: [EffectInvocation] = [ - .managed(.heartbeat(channels: ["c1", "c2"], groups: ["g1", "g2"])) + .regular(.heartbeat(channels: ["c1", "c2"], groups: ["g1", "g2"])) ] let expectedState = Presence.Heartbeating(input: input) @@ -224,7 +247,7 @@ class PresenceTransitionTests: XCTestCase { event: .reconnect ) let expectedInvocations: [EffectInvocation] = [ - .managed(.heartbeat(channels: ["c1", "c2"], groups: ["g1", "g2"])) + .regular(.heartbeat(channels: ["c1", "c2"], groups: ["g1", "g2"])) ] let expectedState = Presence.Heartbeating(input: input) @@ -244,7 +267,7 @@ class PresenceTransitionTests: XCTestCase { event: .disconnect ) let expectedInvocations: [EffectInvocation] = [ - .managed(.leave(channels: ["c1", "c2"], groups: ["g1", "g2"])) + .regular(.leave(channels: ["c1", "c2"], groups: ["g1", "g2"])) ] let expectedState = Presence.HeartbeatStopped(input: input) @@ -263,7 +286,7 @@ class PresenceTransitionTests: XCTestCase { ) let expectedInvocations: [EffectInvocation] = [ .cancel(.scheduleNextHeartbeat), - .managed(.leave(channels: ["c1", "c2"], groups: ["g1", "g2"])) + .regular(.leave(channels: ["c1", "c2"], groups: ["g1", "g2"])) ] let expectedState = Presence.HeartbeatStopped(input: input) @@ -282,7 +305,7 @@ class PresenceTransitionTests: XCTestCase { ) let expectedInvocations: [EffectInvocation] = [ .cancel(.delayedHeartbeat), - .managed(.leave(channels: ["c1", "c2"], groups: ["g1", "g2"])) + .regular(.leave(channels: ["c1", "c2"], groups: ["g1", "g2"])) ] let expectedState = Presence.HeartbeatStopped(input: input) @@ -302,7 +325,7 @@ class PresenceTransitionTests: XCTestCase { event: .heartbeatSuccess ) let expectedInvocations: [EffectInvocation] = [ - .managed(.scheduleNextHeartbeat(channels: ["c1", "c2"], groups: ["g1", "g2"])) + .managed(.wait(channels: ["c1", "c2"], groups: ["g1", "g2"])) ] let expectedState = Presence.HeartbeatCooldown(input: input) diff --git a/Tests/PubNubTests/EventEngine/Subscribe/SubscribeTransitionTests.swift b/Tests/PubNubTests/EventEngine/Subscribe/SubscribeTransitionTests.swift index 10361a2d..4ffe0fcf 100644 --- a/Tests/PubNubTests/EventEngine/Subscribe/SubscribeTransitionTests.swift +++ b/Tests/PubNubTests/EventEngine/Subscribe/SubscribeTransitionTests.swift @@ -30,6 +30,33 @@ import XCTest @testable import PubNub +extension SubscribeState { + func isEqual(to otherState: some SubscribeState) -> Bool { + (otherState as? Self) == self + } +} + +extension Subscribe.Invocation : Equatable { + public static func ==(lhs: Subscribe.Invocation, rhs: Subscribe.Invocation) -> Bool { + switch (lhs, rhs) { + case let (.handshakeRequest(lC, lG), .handshakeRequest(rC, rG)): + return lC.sorted(by: <) == rC.sorted(by: <) && lG.sorted(by: <) == rG.sorted(by: <) + case let (.handshakeReconnect(lC, lG, lAtt, lErr),.handshakeReconnect(rC, rG, rAtt, rErr)): + return lC.sorted(by: <) == rC.sorted(by: <) && lG.sorted(by: <) == rG.sorted(by: <) && lAtt == rAtt && lErr == rErr + case let (.receiveMessages(lC, lG, lCrsr),.receiveMessages(rC, rG, rCrsr)): + return lC.sorted(by: <) == rC.sorted(by: <) && lG.sorted(by: <) == rG.sorted(by: <) && lCrsr == rCrsr + case let (.receiveReconnect(lC, lG, lCrsr, lAtt, lErr), .receiveReconnect(rC, rG, rCrsr, rAtt, rErr)): + return lC.sorted(by: <) == rC.sorted(by: <) && lG.sorted(by: <) == rG.sorted(by: <) && lCrsr == rCrsr && lAtt == rAtt && lErr == rErr + case let (.emitStatus(lhsChange), .emitStatus(rhsChange)): + return lhsChange == rhsChange + case let (.emitMessages(lhsMssgs, lhsCrsr), .emitMessages(rhsMssgs, rhsCrsr)): + return lhsMssgs == rhsMssgs && lhsCrsr == rhsCrsr + default: + return false + } + } +} + class SubscribeTransitionTests: XCTestCase { private let transition = SubscribeTransition() private let input = SubscribeInput(channels: [PubNubChannel(channel: "test-channel")])