From efa12bce11a965142a4536101d99417f02738ed4 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Tue, 27 Dec 2016 15:47:25 +0000 Subject: [PATCH 01/16] RTP2: remove pending --- Spec/RealtimeClientPresence.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Spec/RealtimeClientPresence.swift b/Spec/RealtimeClientPresence.swift index d646a96e6..7d6be5762 100644 --- a/Spec/RealtimeClientPresence.swift +++ b/Spec/RealtimeClientPresence.swift @@ -870,7 +870,7 @@ class RealtimeClientPresence: QuickSpec { it("should be used a PresenceMap to maintain a list of members") { let options = AblyTests.commonAppSetup() var clientSecondary: ARTRealtime! - defer { clientSecondary.close() } + defer { clientSecondary.dispose(); clientSecondary.close() } waitUntil(timeout: testTimeout) { done in clientSecondary = AblyTests.addMembersSequentiallyToChannel("test", members: 100, options: options) { From dbf9144ded0c51dd503bfcc6cfec1b928fe7dbd7 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Wed, 28 Dec 2016 18:40:23 +0000 Subject: [PATCH 02/16] Test suite: set Nimble.AsyncDefaults.Timeout - increase the default timeout value from all async expectations --- Spec/RealtimeClientPresence.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Spec/RealtimeClientPresence.swift b/Spec/RealtimeClientPresence.swift index 7d6be5762..abbebdef8 100644 --- a/Spec/RealtimeClientPresence.swift +++ b/Spec/RealtimeClientPresence.swift @@ -12,6 +12,12 @@ import Nimble import Foundation class RealtimeClientPresence: QuickSpec { + + override func setUp() { + super.setUp() + AsyncDefaults.Timeout = testTimeout + } + override func spec() { describe("Presence") { From db1cbd2234609d1239cdea856177daf934ec82cd Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Tue, 27 Dec 2016 15:47:39 +0000 Subject: [PATCH 03/16] RTP2a --- Spec/RealtimeClientPresence.swift | 48 +++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/Spec/RealtimeClientPresence.swift b/Spec/RealtimeClientPresence.swift index abbebdef8..dd8f3a3b0 100644 --- a/Spec/RealtimeClientPresence.swift +++ b/Spec/RealtimeClientPresence.swift @@ -930,6 +930,54 @@ class RealtimeClientPresence: QuickSpec { expect(user50LeaveTimestamp).to(beGreaterThan(user50PresentTimestamp)) } + // RTP2 + context("PresenceMap") { + + // RTP2a + it("all incoming presence messages must be compared for newness with the matching member already in the PresenceMap") { + let options = AblyTests.commonAppSetup() + let client = ARTRealtime(options: options) + defer { client.dispose(); client.close() } + let channel = client.channels.get("foo") + + waitUntil(timeout: testTimeout) { done in + channel.presence.enterClient("tester", data: nil) { error in + expect(error).to(beNil()) + done() + } + } + + guard let intialPresenceMessage = channel.presenceMap.members["tester"] else { + fail("Missing Presence message"); return + } + + expect(intialPresenceMessage.memberKey()).to(equal("\(client.connection.id):tester")) + + var compareForNewnessMethodCalls = 0 + let hook = channel.presenceMap.testSuite_injectIntoMethodAfter(NSSelectorFromString("compareForNewness")) { + compareForNewnessMethodCalls += 1 + } + defer { hook.remove() } + + waitUntil(timeout: testTimeout) { done in + channel.presence.enterClient("tester", data: nil) { error in + expect(error).to(beNil()) + done() + } + } + + guard let updatedPresenceMessage = channel.presenceMap.members["tester"] else { + fail("Missing Presence message"); return + } + + expect(intialPresenceMessage.memberKey()).to(equal(updatedPresenceMessage.memberKey())) + expect(intialPresenceMessage.timestamp).toNot(equal(updatedPresenceMessage.timestamp)) + + expect(compareForNewnessMethodCalls) == 1 + } + + } + // RTP8 context("enter") { // RTP8h From a605f7611bd2a9218425854e57e392ef555e7a1a Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Tue, 27 Dec 2016 19:44:55 +0000 Subject: [PATCH 04/16] RTP2b --- Spec/RealtimeClientPresence.swift | 88 +++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/Spec/RealtimeClientPresence.swift b/Spec/RealtimeClientPresence.swift index dd8f3a3b0..ba4d9d894 100644 --- a/Spec/RealtimeClientPresence.swift +++ b/Spec/RealtimeClientPresence.swift @@ -976,6 +976,94 @@ class RealtimeClientPresence: QuickSpec { expect(compareForNewnessMethodCalls) == 1 } + // RTP2b + context("compare for newness") { + + // RTP2b1 + it("presence message has a connectionId which is not an initial substring of its id") { + let options = AblyTests.commonAppSetup() + + let clientSubscribed = ARTRealtime(options: options) + defer { clientSubscribed.dispose(); clientSubscribed.close() } + let channelSubscribed = clientSubscribed.channels.get("foo") + channelSubscribed.attach() + + let clientPresentMember = ARTRealtime(options: options) + defer { clientPresentMember.dispose(); clientPresentMember.close() } + let channelPresentMember = clientPresentMember.channels.get("foo") + + var hasInconsistentConnectionIdMethodCalls = 0 + let hook = channelSubscribed.presenceMap.testSuite_injectIntoMethodAfter(NSSelectorFromString("hasInconsistentConnectionId")) { + hasInconsistentConnectionIdMethodCalls += 1 + } + defer { hook.remove() } + + waitUntil(timeout: testTimeout) { done in + channelPresentMember.presence.enterClient("tester", data: nil) { error in + expect(error).to(beNil()) + done() + } + } + + waitUntil(timeout: testTimeout) { done in + channelSubscribed.presence.get { presences, error in + expect(error).to(beNil()) + expect(presences).to(haveCount(1)) + done() + } + } + + waitUntil(timeout: testTimeout) { done in + channelSubscribed.presence.subscribe(.Leave) { presence in + // Check `synthesized leave` event + expect(presence.id).toNot(equal("\(presence.connectionId):0:0")) + done() + } + clientPresentMember.close() + } + + expect(channelSubscribed.presenceMap.members).to(beEmpty()) + expect(hasInconsistentConnectionIdMethodCalls) == 1 + } + + // RTP2b2 + it("split the id of both presence messages") { + let options = AblyTests.commonAppSetup() + let client = ARTRealtime(options: options) + defer { client.dispose(); client.close() } + let channel = client.channels.get("foo") + + var compareByMsgSerialAndOrIndexMethodCalls = 0 + let hook = channel.presenceMap.testSuite_injectIntoMethodAfter(NSSelectorFromString("compareByMsgSerialAndOrIndex")) { + compareByMsgSerialAndOrIndexMethodCalls += 1 + } + defer { hook.remove() } + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) + channel.presence.enterClient("tester", data: nil) { error in + expect(error).to(beNil()) + partialDone() + } + channel.presence.subscribe(.Enter) { presence in + expect(NSRegularExpression.extract(presence.id, pattern: ":\\d*:\\d*")) == ":0:0" + partialDone() + } + channel.presence.subscribe(.Leave) { presence in + expect(NSRegularExpression.extract(presence.id, pattern: ":\\d*:\\d*")) == ":0:1" + partialDone() + } + channel.presence.leaveClient("tester", data: nil) { error in + expect(error).to(beNil()) + partialDone() + } + } + + expect(compareByMsgSerialAndOrIndexMethodCalls) == 1 + } + + } + } // RTP8 From 9405ca74995eabdfaae068fc1b8c41f54706c468 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Wed, 28 Dec 2016 18:49:38 +0000 Subject: [PATCH 05/16] RTP2c --- Spec/RealtimeClientPresence.swift | 113 ++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/Spec/RealtimeClientPresence.swift b/Spec/RealtimeClientPresence.swift index ba4d9d894..2ab7f7cd4 100644 --- a/Spec/RealtimeClientPresence.swift +++ b/Spec/RealtimeClientPresence.swift @@ -1064,6 +1064,119 @@ class RealtimeClientPresence: QuickSpec { } + // RTP2c + context("all presence messages from a SYNC must also be compared for newness in the same way as they would from a PRESENCE") { + + it("discard members where messages have arrived before the SYNC") { + let options = AblyTests.commonAppSetup() + let timeBeforeSync = NSDate() + + var clientMembers: ARTRealtime? + defer { clientMembers?.dispose(); clientMembers?.close() } + waitUntil(timeout: testTimeout) { done in + clientMembers = AblyTests.addMembersSequentiallyToChannel("foo", members: 120, options: options) { + done() + }.first + } + guard let membersConnectionId = clientMembers?.connection.id else { + fail("Members client isn't connected"); return + } + + let client = AblyTests.newRealtime(options) + defer { client.dispose(); client.close() } + let channel = client.channels.get("foo") + + guard let transport = client.transport as? TestProxyTransport else { + fail("TestProxyTransport is not set"); return + } + + channel.presence.subscribe(.Leave) { leave in + expect(leave.clientId).to(equal("user110")) + fail("Should not fire Leave event for member `user110` because it's out of date") + } + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(3, done: done) + transport.beforeProcessingReceivedMessage = { protocolMessage in + if protocolMessage.action == .Sync { + let injectLeave = ARTPresenceMessage() + injectLeave.action = .Leave + injectLeave.connectionId = membersConnectionId + injectLeave.clientId = "user110" + injectLeave.timestamp = timeBeforeSync + protocolMessage.presence?.append(injectLeave) + transport.beforeProcessingReceivedMessage = nil + partialDone() + } + } + channel.presenceMap.testSuite_injectIntoMethodAfter(#selector(ARTPresenceMap.endSync)) { + expect(channel.presenceMap.syncInProgress).to(beFalse()) + expect(channel.presenceMap.members).to(haveCount(120)) + expect(channel.presenceMap.members.filter{ _, presence in presence.clientId == "user110" && presence.action == .Present }).to(haveCount(1)) + partialDone() + } + channel.attach() { error in + expect(error).to(beNil()) + partialDone() + } + } + } + + it("accept members where message have arrived after the SYNC") { + let options = AblyTests.commonAppSetup() + + var clientMembers: ARTRealtime? + defer { clientMembers?.dispose(); clientMembers?.close() } + waitUntil(timeout: testTimeout) { done in + clientMembers = AblyTests.addMembersSequentiallyToChannel("foo", members: 120, options: options) { + done() + }.first + } + guard let membersConnectionId = clientMembers?.connection.id else { + fail("Members client isn't connected"); return + } + + let client = AblyTests.newRealtime(options) + defer { client.dispose(); client.close() } + let channel = client.channels.get("foo") + + guard let transport = client.transport as? TestProxyTransport else { + fail("TestProxyTransport is not set"); return + } + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(4, done: done) + channel.presence.subscribe(.Leave) { leave in + expect(leave.clientId).to(equal("user110")) + partialDone() + } + transport.beforeProcessingReceivedMessage = { protocolMessage in + if protocolMessage.action == .Sync { + let injectLeave = ARTPresenceMessage() + injectLeave.action = .Leave + injectLeave.connectionId = membersConnectionId + injectLeave.clientId = "user110" + injectLeave.timestamp = NSDate() + 1 + protocolMessage.presence?.append(injectLeave) + transport.beforeProcessingReceivedMessage = nil + partialDone() + } + } + channel.presenceMap.testSuite_injectIntoMethodAfter(#selector(ARTPresenceMap.endSync)) { + expect(channel.presenceMap.syncInProgress).to(beFalse()) + expect(channel.presenceMap.members).to(haveCount(119)) + expect(channel.presenceMap.members.filter{ _, presence in presence.clientId == "user110" }).to(beEmpty()) + partialDone() + } + channel.attach() { error in + expect(error).to(beNil()) + partialDone() + } + } + } + + } + } // RTP8 From 7cca55224b67d213dac4e9957756156ee63972b2 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Wed, 28 Dec 2016 18:49:58 +0000 Subject: [PATCH 06/16] RTP2d --- Spec/RealtimeClientPresence.swift | 77 +++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/Spec/RealtimeClientPresence.swift b/Spec/RealtimeClientPresence.swift index 2ab7f7cd4..a1c5c6135 100644 --- a/Spec/RealtimeClientPresence.swift +++ b/Spec/RealtimeClientPresence.swift @@ -1177,6 +1177,83 @@ class RealtimeClientPresence: QuickSpec { } + // RTP2d + it("if action of ENTER arrives, it should be added to the presence map with the action set to PRESENT") { + let options = AblyTests.commonAppSetup() + let client = ARTRealtime(options: options) + defer { client.dispose(); client.close() } + let channel = client.channels.get("foo") + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) + channel.presence.subscribe(.Enter) { _ in + partialDone() + } + channel.presence.enterClient("tester", data: nil) { error in + expect(error).to(beNil()) + partialDone() + } + } + + expect(channel.presenceMap.members.filter{ _, presence in presence.action == .Present }).to(haveCount(1)) + expect(channel.presenceMap.members.filter{ _, presence in presence.action == .Enter }).to(beEmpty()) + } + + // RTP2d + it("if action of UPDATE arrives, it should be added to the presence map with the action set to PRESENT") { + let options = AblyTests.commonAppSetup() + let client = ARTRealtime(options: options) + defer { client.dispose(); client.close() } + let channel = client.channels.get("foo") + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(3, done: done) + channel.presence.subscribe(.Update) { _ in + partialDone() + } + channel.presence.enterClient("tester", data: nil) { error in + expect(error).to(beNil()) + partialDone() + } + } + + expect(channel.presenceMap.members).to(haveCount(1)) + expect(channel.presenceMap.members.filter{ _, presence in presence.action == .Present }).to(haveCount(1)) + expect(channel.presenceMap.members.filter{ _, presence in presence.action == .Update }).to(beEmpty()) + } + + // RTP2d + it("if action of PRESENT arrives, it should be added to the presence map with the action set to PRESENT") { + let options = AblyTests.commonAppSetup() + + var clientMembers: ARTRealtime! + defer { clientMembers.dispose(); clientMembers.close() } + waitUntil(timeout: testTimeout) { done in + clientMembers = AblyTests.addMembersSequentiallyToChannel("foo", members: 1, options: options) { + done() + }.first + } + + let client = ARTRealtime(options: options) + defer { client.dispose(); client.close() } + let channel = client.channels.get("foo") + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) + channel.presenceMap.testSuite_injectIntoMethodAfter(#selector(ARTPresenceMap.endSync)) { + expect(channel.presenceMap.syncInProgress).to(beFalse()) + partialDone() + } + channel.attach() { error in + expect(error).to(beNil()) + partialDone() + } + } + + expect(channel.presenceMap.members).to(haveCount(1)) + expect(channel.presenceMap.members.filter{ _, presence in presence.action == .Present }).to(haveCount(1)) + } + } // RTP8 From e52ffb6082539047c9c96951ba11906a43b89848 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Wed, 28 Dec 2016 18:50:07 +0000 Subject: [PATCH 07/16] RTP2e --- Spec/RealtimeClientPresence.swift | 45 +++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/Spec/RealtimeClientPresence.swift b/Spec/RealtimeClientPresence.swift index a1c5c6135..4a6b957a4 100644 --- a/Spec/RealtimeClientPresence.swift +++ b/Spec/RealtimeClientPresence.swift @@ -1254,6 +1254,51 @@ class RealtimeClientPresence: QuickSpec { expect(channel.presenceMap.members.filter{ _, presence in presence.action == .Present }).to(haveCount(1)) } + // RTP2e + it("if a SYNC is not in progress, then when a presence message with an action of LEAVE arrives, that memberKey should be deleted from the presence map, if present") { + let options = AblyTests.commonAppSetup() + + var clientMembers: ARTRealtime? + defer { clientMembers?.dispose(); clientMembers?.close() } + waitUntil(timeout: testTimeout) { done in + clientMembers = AblyTests.addMembersSequentiallyToChannel("foo", members: 20, options: options) { + done() + }.first + } + + let client = AblyTests.newRealtime(options) + defer { client.dispose(); client.close() } + let channel = client.channels.get("foo") + channel.attach() + + guard let transport = client.transport as? TestProxyTransport else { + fail("TestProxyTransport is not set"); return + } + waitUntil(timeout: testTimeout) { done in + transport.afterProcessingReceivedMessage = { protocolMessage in + if protocolMessage.action == .Sync { + done() + } + } + } + + expect(channel.presenceMap.syncInProgress).toEventually(beFalse(), timeout: testTimeout) + + guard let user11MemberKey = channel.presenceMap.members["user11"]?.memberKey() else { + fail("user11 memberKey is not present"); return + } + + waitUntil(timeout: testTimeout) { done in + channel.presence.subscribe(.Leave) { presence in + expect(presence.clientId).to(equal("user11")) + done() + } + clientMembers?.channels.get("foo").presence.leaveClient("user11", data: nil) + } + + expect(channel.presenceMap.members.filter{ _, presence in presence.memberKey() == user11MemberKey }).to(beEmpty()) + } + } // RTP8 From a6ebc02b2249f01b499ab7517e410bdadf18aeb2 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Wed, 28 Dec 2016 18:50:18 +0000 Subject: [PATCH 08/16] RTP2f --- Spec/RealtimeClientPresence.swift | 47 +++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/Spec/RealtimeClientPresence.swift b/Spec/RealtimeClientPresence.swift index 4a6b957a4..58e9af3fe 100644 --- a/Spec/RealtimeClientPresence.swift +++ b/Spec/RealtimeClientPresence.swift @@ -1299,6 +1299,53 @@ class RealtimeClientPresence: QuickSpec { expect(channel.presenceMap.members.filter{ _, presence in presence.memberKey() == user11MemberKey }).to(beEmpty()) } + // RTP2f + it("if a SYNC is in progress, then when a presence message with an action of LEAVE arrives, it should be stored in the presence map with the action set to ABSENT") { + let options = AblyTests.commonAppSetup() + + var clientMembers: ARTRealtime? + defer { clientMembers?.dispose(); clientMembers?.close() } + waitUntil(timeout: testTimeout) { done in + clientMembers = AblyTests.addMembersSequentiallyToChannel("foo", members: 20, options: options) { + done() + }.first + } + + let client = AblyTests.newRealtime(options) + defer { client.dispose(); client.close() } + let channel = client.channels.get("foo") + channel.attach() + + guard let transport = client.transport as? TestProxyTransport else { + fail("TestProxyTransport is not set"); return + } + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(3, done: done) + channel.presenceMap.testSuite_injectIntoMethodAfter(#selector(ARTPresenceMap.startSync)) { + expect(channel.presenceMap.syncInProgress).to(beTrue()) + clientMembers?.channels.get("foo").presence.leaveClient("user11", data: nil) { _ in + partialDone() + } + transport.afterProcessingReceivedMessage = { protocolMessage in + if protocolMessage.action == .Presence && protocolMessage.presence?[0].action == .Some(.Leave) { + expect(protocolMessage.presence).to(haveCount(1)) + partialDone() + } + } + } + channel.presenceMap.testSuite_injectIntoMethodAfter(#selector(ARTPresenceMap.endSync)) { + expect(channel.presenceMap.syncInProgress).to(beFalse()) + expect(channel.presenceMap.members.filter{ _, presence in presence.action == .Absent }).to(beEmpty()) + partialDone() + } + } + + expect(channel.presenceMap.members.filter{ _, presence in presence.action == .Absent }).to(haveCount(1)) + + expect(channel.presenceMap.members).to(haveCount(19)) + } + } // RTP8 From 1ea49d351951ac9a2b26b99afdbe44a5c931abf4 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Wed, 28 Dec 2016 18:50:24 +0000 Subject: [PATCH 09/16] RTP2g --- Spec/RealtimeClientPresence.swift | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/Spec/RealtimeClientPresence.swift b/Spec/RealtimeClientPresence.swift index 58e9af3fe..f963318e0 100644 --- a/Spec/RealtimeClientPresence.swift +++ b/Spec/RealtimeClientPresence.swift @@ -1346,6 +1346,28 @@ class RealtimeClientPresence: QuickSpec { expect(channel.presenceMap.members).to(haveCount(19)) } + // RTP2g + it("any incoming presence message that passes the newness check should be emitted on the Presence object, with an event name set to its original action") { + let options = AblyTests.commonAppSetup() + let client = ARTRealtime(options: options) + defer { client.dispose(); client.close() } + let channel = client.channels.get("foo") + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) + channel.presence.enterClient("tester", data: nil) { error in + expect(error).to(beNil()) + partialDone() + } + channel.presence.subscribe(.Enter) { _ in + partialDone() + } + } + + expect(channel.presenceMap.members.filter{ _, presence in presence.action == .Present }).to(haveCount(1)) + expect(channel.presenceMap.members.filter{ _, presence in presence.action == .Enter }).to(beEmpty()) + } + } // RTP8 From c0442eb6bdb076cf0555ffc8b7d1fafa0890fb96 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Tue, 3 Jan 2017 16:43:46 +0000 Subject: [PATCH 10/16] Test suite: ARTPresenceMessage convenience initializer --- Spec/TestUtilities.swift | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Spec/TestUtilities.swift b/Spec/TestUtilities.swift index 47fdb99b7..aa4272ae3 100644 --- a/Spec/TestUtilities.swift +++ b/Spec/TestUtilities.swift @@ -1054,6 +1054,19 @@ extension ARTAuth { } +extension ARTPresenceMessage { + + convenience init(clientId: String, action: ARTPresenceAction, connectionId: String, id: String, timestamp: NSDate = NSDate()) { + self.init() + self.action = action + self.clientId = clientId + self.connectionId = connectionId + self.id = id + self.timestamp = timestamp + } + +} + extension ARTRealtimeConnectionState : CustomStringConvertible { public var description : String { return ARTRealtimeConnectionStateToStr(self) From f43f81b5e070b459d0e02f9e7673f137d0c9e961 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Tue, 3 Jan 2017 16:45:03 +0000 Subject: [PATCH 11/16] Test suite: NSDate custom operators - convenience for use of `dateByAddingTimeInterval` --- Spec/TestUtilities.swift | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Spec/TestUtilities.swift b/Spec/TestUtilities.swift index aa4272ae3..e3eec9e7c 100644 --- a/Spec/TestUtilities.swift +++ b/Spec/TestUtilities.swift @@ -947,6 +947,14 @@ public func >=(lhs: NSDate, rhs: NSDate) -> Bool { return (lhs > rhs || lhs == rhs) } +public func -(lhs: NSDate, rhs: NSTimeInterval) -> NSDate { + return lhs.dateByAddingTimeInterval(-rhs) +} + +public func +(lhs: NSDate, rhs: NSTimeInterval) -> NSDate { + return lhs.dateByAddingTimeInterval(rhs) +} + extension NSRegularExpression { class func match(value: String?, pattern: String) -> Bool { From d74e24a20bc93607253525a87e46dcb2e46985f8 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Tue, 3 Jan 2017 16:47:07 +0000 Subject: [PATCH 12/16] Fix RTP2b1 --- Spec/RealtimeClientPresence.swift | 109 ++++++++++++++++++++---------- 1 file changed, 73 insertions(+), 36 deletions(-) diff --git a/Spec/RealtimeClientPresence.swift b/Spec/RealtimeClientPresence.swift index f963318e0..71423ef5f 100644 --- a/Spec/RealtimeClientPresence.swift +++ b/Spec/RealtimeClientPresence.swift @@ -979,51 +979,88 @@ class RealtimeClientPresence: QuickSpec { // RTP2b context("compare for newness") { - // RTP2b1 - it("presence message has a connectionId which is not an initial substring of its id") { - let options = AblyTests.commonAppSetup() + context("presence message has a connectionId which is not an initial substring of its id") { + // RTP2b1 + it("compares them by timestamp numerically") { + let options = AblyTests.commonAppSetup() + let now = NSDate() + + var clientMembers: ARTRealtime? + defer { clientMembers?.dispose(); clientMembers?.close() } + waitUntil(timeout: testTimeout) { done in + clientMembers = AblyTests.addMembersSequentiallyToChannel("foo", members: 100, options: options) { + done() + }.first + } - let clientSubscribed = ARTRealtime(options: options) - defer { clientSubscribed.dispose(); clientSubscribed.close() } - let channelSubscribed = clientSubscribed.channels.get("foo") - channelSubscribed.attach() + let clientSubscribed = AblyTests.newRealtime(options) + defer { clientSubscribed.dispose(); clientSubscribed.close() } + let channelSubscribed = clientSubscribed.channels.get("foo") + + let presenceData: [ARTPresenceMessage] = [ + ARTPresenceMessage(clientId: "a", action: .Enter, connectionId: "one", id: "one:0:0", timestamp: now), + ARTPresenceMessage(clientId: "a", action: .Leave, connectionId: "one", id: "fabricated:0:1", timestamp: now + 1), + ARTPresenceMessage(clientId: "b", action: .Enter, connectionId: "one", id: "one:0:2", timestamp: now), + ARTPresenceMessage(clientId: "b", action: .Leave, connectionId: "one", id: "fabricated:0:3", timestamp: now - 1), + ARTPresenceMessage(clientId: "c", action: .Enter, connectionId: "one", id: "fabricated:0:4", timestamp: now), + ARTPresenceMessage(clientId: "c", action: .Leave, connectionId: "one", id: "fabricated:0:5", timestamp: now - 1), + ] + + guard let transport = clientSubscribed.transport as? TestProxyTransport else { + fail("TestProxyTransport is not set"); return + } - let clientPresentMember = ARTRealtime(options: options) - defer { clientPresentMember.dispose(); clientPresentMember.close() } - let channelPresentMember = clientPresentMember.channels.get("foo") + waitUntil(timeout: testTimeout) { done in + transport.afterProcessingReceivedMessage = { protocolMessage in + // Receive the first Sync message from Ably service + if protocolMessage.action == .Sync { - var hasInconsistentConnectionIdMethodCalls = 0 - let hook = channelSubscribed.presenceMap.testSuite_injectIntoMethodAfter(NSSelectorFromString("hasInconsistentConnectionId")) { - hasInconsistentConnectionIdMethodCalls += 1 - } - defer { hook.remove() } + // Inject a fabricated Presence message + let presenceMessage = ARTProtocolMessage() + presenceMessage.action = .Presence + presenceMessage.channel = protocolMessage.channel + presenceMessage.connectionSerial = protocolMessage.connectionSerial + 1 + presenceMessage.timestamp = NSDate() + presenceMessage.presence = presenceData - waitUntil(timeout: testTimeout) { done in - channelPresentMember.presence.enterClient("tester", data: nil) { error in - expect(error).to(beNil()) - done() - } - } + transport.receive(presenceMessage) - waitUntil(timeout: testTimeout) { done in - channelSubscribed.presence.get { presences, error in - expect(error).to(beNil()) - expect(presences).to(haveCount(1)) - done() + // Simulate an end to the sync + let endSyncMessage = ARTProtocolMessage() + endSyncMessage.action = .Sync + endSyncMessage.channel = protocolMessage.channel + endSyncMessage.channelSerial = "validserialprefix:" //with no part after the `:` this indicates the end to the SYNC + endSyncMessage.connectionSerial = protocolMessage.connectionSerial + 2 + endSyncMessage.timestamp = NSDate() + + transport.afterProcessingReceivedMessage = nil + transport.receive(endSyncMessage) + + // Stop the next sync message from Ably service because we already injected the end of the sync + transport.actionsIgnored = [.Sync] + + done() + } + } + channelSubscribed.attach() } - } - waitUntil(timeout: testTimeout) { done in - channelSubscribed.presence.subscribe(.Leave) { presence in - // Check `synthesized leave` event - expect(presence.id).toNot(equal("\(presence.connectionId):0:0")) - done() + waitUntil(timeout: testTimeout) { done in + channelSubscribed.presence.get { members, error in + expect(error).to(beNil()) + guard let members = members else { + fail("Members is nil"); done(); return + } + expect(members).to(haveCount(102)) //100 initial members + "b" + "c", client "a" is discarded + expect(members.filter{ $0.clientId == "a" }).to(beEmpty()) + expect(members.filter{ $0.clientId == "b" }).to(haveCount(1)) + expect(members.filter{ $0.clientId == "b" }.first?.timestamp).to(equal(now)) + expect(members.filter{ $0.clientId == "c" }).to(haveCount(1)) + expect(members.filter{ $0.clientId == "c" }.first?.timestamp).to(equal(now)) + done() + } } - clientPresentMember.close() } - - expect(channelSubscribed.presenceMap.members).to(beEmpty()) - expect(hasInconsistentConnectionIdMethodCalls) == 1 } // RTP2b2 From 1b7750e0d2553fb00c8724e27cc9da36e0b05eba Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Tue, 3 Jan 2017 17:07:43 +0000 Subject: [PATCH 13/16] Fix RTP2b2 --- Spec/RealtimeClientPresence.swift | 92 +++++++++++++++++++++++-------- 1 file changed, 69 insertions(+), 23 deletions(-) diff --git a/Spec/RealtimeClientPresence.swift b/Spec/RealtimeClientPresence.swift index 71423ef5f..211b0feb2 100644 --- a/Spec/RealtimeClientPresence.swift +++ b/Spec/RealtimeClientPresence.swift @@ -1066,37 +1066,83 @@ class RealtimeClientPresence: QuickSpec { // RTP2b2 it("split the id of both presence messages") { let options = AblyTests.commonAppSetup() - let client = ARTRealtime(options: options) - defer { client.dispose(); client.close() } - let channel = client.channels.get("foo") + let now = NSDate() + + var clientMembers: ARTRealtime? + defer { clientMembers?.dispose(); clientMembers?.close() } + waitUntil(timeout: testTimeout) { done in + clientMembers = AblyTests.addMembersSequentiallyToChannel("foo", members: 100, options: options) { + done() + }.first + } + + let clientSubscribed = AblyTests.newRealtime(options) + defer { clientSubscribed.dispose(); clientSubscribed.close() } + let channelSubscribed = clientSubscribed.channels.get("foo") + + let presenceData: [ARTPresenceMessage] = [ + ARTPresenceMessage(clientId: "a", action: .Enter, connectionId: "one", id: "one:0:0", timestamp: now), + ARTPresenceMessage(clientId: "a", action: .Leave, connectionId: "one", id: "one:1:0", timestamp: now - 1), + ARTPresenceMessage(clientId: "b", action: .Enter, connectionId: "one", id: "one:2:2", timestamp: now), + ARTPresenceMessage(clientId: "b", action: .Leave, connectionId: "one", id: "one:2:1", timestamp: now + 1), + ARTPresenceMessage(clientId: "c", action: .Enter, connectionId: "one", id: "one:4:4", timestamp: now), + ARTPresenceMessage(clientId: "c", action: .Leave, connectionId: "one", id: "one:3:5", timestamp: now + 1), + ] - var compareByMsgSerialAndOrIndexMethodCalls = 0 - let hook = channel.presenceMap.testSuite_injectIntoMethodAfter(NSSelectorFromString("compareByMsgSerialAndOrIndex")) { - compareByMsgSerialAndOrIndexMethodCalls += 1 + guard let transport = clientSubscribed.transport as? TestProxyTransport else { + fail("TestProxyTransport is not set"); return } - defer { hook.remove() } waitUntil(timeout: testTimeout) { done in - let partialDone = AblyTests.splitDone(2, done: done) - channel.presence.enterClient("tester", data: nil) { error in - expect(error).to(beNil()) - partialDone() - } - channel.presence.subscribe(.Enter) { presence in - expect(NSRegularExpression.extract(presence.id, pattern: ":\\d*:\\d*")) == ":0:0" - partialDone() - } - channel.presence.subscribe(.Leave) { presence in - expect(NSRegularExpression.extract(presence.id, pattern: ":\\d*:\\d*")) == ":0:1" - partialDone() + transport.afterProcessingReceivedMessage = { protocolMessage in + // Receive the first Sync message from Ably service + if protocolMessage.action == .Sync { + + // Inject a fabricated Presence message + let presenceMessage = ARTProtocolMessage() + presenceMessage.action = .Presence + presenceMessage.channel = protocolMessage.channel + presenceMessage.connectionSerial = protocolMessage.connectionSerial + 1 + presenceMessage.timestamp = NSDate() + presenceMessage.presence = presenceData + + transport.receive(presenceMessage) + + // Simulate an end to the sync + let endSyncMessage = ARTProtocolMessage() + endSyncMessage.action = .Sync + endSyncMessage.channel = protocolMessage.channel + endSyncMessage.channelSerial = "validserialprefix:" //with no part after the `:` this indicates the end to the SYNC + endSyncMessage.connectionSerial = protocolMessage.connectionSerial + 2 + endSyncMessage.timestamp = NSDate() + + transport.afterProcessingReceivedMessage = nil + transport.receive(endSyncMessage) + + // Stop the next sync message from Ably service because we already injected the end of the sync + transport.actionsIgnored = [.Sync] + + done() + } } - channel.presence.leaveClient("tester", data: nil) { error in + channelSubscribed.attach() + } + + waitUntil(timeout: testTimeout) { done in + channelSubscribed.presence.get { members, error in expect(error).to(beNil()) - partialDone() + guard let members = members else { + fail("Members is nil"); done(); return + } + expect(members).to(haveCount(102)) //100 initial members + "b" + "c", client "a" is discarded + expect(members.filter{ $0.clientId == "a" }).to(beEmpty()) + expect(members.filter{ $0.clientId == "b" }).to(haveCount(1)) + expect(members.filter{ $0.clientId == "b" }.first?.timestamp).to(equal(now)) + expect(members.filter{ $0.clientId == "c" }).to(haveCount(1)) + expect(members.filter{ $0.clientId == "c" }.first?.timestamp).to(equal(now)) + done() } } - - expect(compareByMsgSerialAndOrIndexMethodCalls) == 1 } } From da7ab94bd7684718b29c308927f37ca6a067185e Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Mon, 23 Jan 2017 23:57:09 +0000 Subject: [PATCH 14/16] PresenceMap: compare for newness --- Source/ARTPresenceMap.h | 6 +++- Source/ARTPresenceMap.m | 68 ++++++++++++++++++++++++++++++++++--- Source/ARTPresenceMessage.h | 2 ++ Source/ARTPresenceMessage.m | 29 ++++++++++++++++ Source/ARTRealtimeChannel.m | 25 ++++++++------ 5 files changed, 114 insertions(+), 16 deletions(-) diff --git a/Source/ARTPresenceMap.h b/Source/ARTPresenceMap.h index bbf3e7c7e..91746b42a 100644 --- a/Source/ARTPresenceMap.h +++ b/Source/ARTPresenceMap.h @@ -11,6 +11,7 @@ @class ARTPresenceMessage; @class ARTErrorInfo; +@class ARTLog; ART_ASSUME_NONNULL_BEGIN @@ -26,7 +27,10 @@ ART_ASSUME_NONNULL_BEGIN @property (readonly, nonatomic, assign) BOOL syncComplete; @property (readonly, nonatomic, getter=getSyncInProgress) BOOL syncInProgress; -- (void)put:(ARTPresenceMessage *)message; +- (instancetype)init UNAVAILABLE_ATTRIBUTE; +- (instancetype)initWithLogger:(ARTLog *)logger; + +- (BOOL)add:(ARTPresenceMessage *)message; - (void)clean; - (void)startSync; diff --git a/Source/ARTPresenceMap.m b/Source/ARTPresenceMap.m index 98144b98f..ea5628c5f 100644 --- a/Source/ARTPresenceMap.m +++ b/Source/ARTPresenceMap.m @@ -9,6 +9,7 @@ #import "ARTPresenceMap.h" #import "ARTPresenceMessage.h" #import "ARTEventEmitter.h" +#import "ARTLog.h" typedef NS_ENUM(NSUInteger, ARTPresenceSyncState) { ARTPresenceSyncStarted, //ItemType: nil @@ -25,11 +26,14 @@ @interface ARTPresenceMap () { @end -@implementation ARTPresenceMap +@implementation ARTPresenceMap { + __weak ARTLog *_logger; +} -- (id)init { +- (instancetype)initWithLogger:(ARTLog *)logger { self = [super init]; if(self) { + _logger = logger; _recentMembers = [NSMutableDictionary dictionary]; _syncStarted = false; _syncComplete = false; @@ -42,11 +46,65 @@ - (id)init { return self.recentMembers; } -- (void)put:(ARTPresenceMessage *)message { +- (BOOL)add:(ARTPresenceMessage *)message { ARTPresenceMessage *latest = [self.recentMembers objectForKey:message.clientId]; - if (!latest || !message.timestamp || [latest.timestamp timeIntervalSince1970] <= [message.timestamp timeIntervalSince1970]) { - [self.recentMembers setObject:message forKey:message.clientId]; + if ([self isNewestPresence:message comparingWith:latest]) { + ARTPresenceMessage *messageCopy = [message copy]; + switch (message.action) { + case ARTPresenceEnter: + case ARTPresenceUpdate: + messageCopy.action = ARTPresencePresent; + break; + case ARTPresenceLeave: + if (self.syncInProgress) { + messageCopy.action = ARTPresenceAbsent; + } + break; + default: + break; + } + [self.recentMembers setObject:messageCopy forKey:message.clientId]; + return YES; + } + return NO; +} + +- (BOOL)isNewestPresence:(nonnull ARTPresenceMessage *)received comparingWith:(ARTPresenceMessage *)latest __attribute__((warn_unused_result)) { + if (latest == nil) { + return YES; + } + + NSArray *receivedMessageIdParts = [received.id componentsSeparatedByCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@":"]]; + if (receivedMessageIdParts.count != 3) { + [_logger error:@"Received presence message id is invalid %@", received.id]; + return !received.timestamp || + [latest.timestamp timeIntervalSince1970] <= [received.timestamp timeIntervalSince1970]; } + NSString *receivedConnectionId = [receivedMessageIdParts objectAtIndex:0]; + NSInteger receivedMsgSerial = [[receivedMessageIdParts objectAtIndex:1] integerValue]; + NSInteger receivedIndex = [[receivedMessageIdParts objectAtIndex:2] integerValue]; + + if ([receivedConnectionId isEqualToString:received.connectionId]) { + NSArray *latestRegisteredIdParts = [latest.id componentsSeparatedByCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@":"]]; + if (latestRegisteredIdParts.count != 3) { + [_logger error:@"Latest registered presence message id is invalid %@", latest.id]; + return !received.timestamp || + [latest.timestamp timeIntervalSince1970] <= [received.timestamp timeIntervalSince1970]; + } + NSInteger latestRegisteredMsgSerial = [[latestRegisteredIdParts objectAtIndex:1] integerValue]; + NSInteger latestRegisteredIndex = [[latestRegisteredIdParts objectAtIndex:2] integerValue]; + + if (receivedMsgSerial > latestRegisteredMsgSerial) { + return YES; + } + else if (receivedMsgSerial == latestRegisteredMsgSerial && receivedIndex > latestRegisteredIndex) { + return YES; + } + return NO; + } + + return !received.timestamp || + [latest.timestamp timeIntervalSince1970] <= [received.timestamp timeIntervalSince1970]; } - (void)clean { diff --git a/Source/ARTPresenceMessage.h b/Source/ARTPresenceMessage.h index 69c3629d4..26e8d162b 100644 --- a/Source/ARTPresenceMessage.h +++ b/Source/ARTPresenceMessage.h @@ -24,4 +24,6 @@ typedef NS_ENUM(NSUInteger, ARTPresenceAction) { - (NSString *)memberKey; +- (BOOL)isEqualToPresenceMessage:(ARTPresenceMessage *)presence; + @end diff --git a/Source/ARTPresenceMessage.m b/Source/ARTPresenceMessage.m index 774ba3ede..1b097e7fc 100644 --- a/Source/ARTPresenceMessage.m +++ b/Source/ARTPresenceMessage.m @@ -38,4 +38,33 @@ - (NSString *)memberKey { return [NSString stringWithFormat:@"%@:%@", self.connectionId, self.clientId]; } +- (BOOL)isEqualToPresenceMessage:(ARTPresenceMessage *)presence { + if (!presence) { + return NO; + } + + BOOL haveEqualConnectionId = (!self.connectionId && !presence.connectionId) || [self.connectionId isEqualToString:presence.connectionId]; + BOOL haveEqualCliendId = (!self.clientId && !presence.clientId) || [self.clientId isEqualToString:presence.clientId]; + + return haveEqualConnectionId && haveEqualCliendId; +} + +#pragma mark - NSObject + +- (BOOL)isEqual:(id)object { + if (self == object) { + return YES; + } + + if (![object isKindOfClass:[ARTPresenceMessage class]]) { + return NO; + } + + return [self isEqualToPresenceMessage:(ARTPresenceMessage *)object]; +} + +- (NSUInteger)hash { + return [self.connectionId hash] ^ [self.clientId hash]; +} + @end diff --git a/Source/ARTRealtimeChannel.m b/Source/ARTRealtimeChannel.m index 7189aeaf0..4d4a42d51 100644 --- a/Source/ARTRealtimeChannel.m +++ b/Source/ARTRealtimeChannel.m @@ -47,7 +47,7 @@ - (instancetype)initWithRealtime:(ARTRealtime *)realtime andName:(NSString *)nam _state = ARTRealtimeChannelInitialized; _queuedMessages = [NSMutableArray array]; _attachSerial = nil; - _presenceMap = [[ARTPresenceMap alloc] init]; + _presenceMap = [[ARTPresenceMap alloc] initWithLogger:self.logger]; _lastPresenceAction = ARTPresenceAbsent; _statesEventEmitter = [[ARTEventEmitter alloc] init]; @@ -64,6 +64,10 @@ + (instancetype)channelWithRealtime:(ARTRealtime *)realtime andName:(NSString *) return [[ARTRealtimeChannel alloc] initWithRealtime:realtime andName:name withOptions:options]; } +- (ARTLog *)getLogger { + return _realtime.logger; +} + - (ARTRealtimePresence *)getPresence { if (!_realtimePresence) { _realtimePresence = [[ARTRealtimePresence alloc] initWithChannel:self]; @@ -85,8 +89,8 @@ - (void)internalPostMessages:(id)data callback:(void (^)(ARTErrorInfo *__art_nul } - (void)requestContinueSync { - [self.logger info:@"R:%p C:%p ARTRealtime requesting to continue sync operation after reconnect", _realtime, self]; - + [self.logger debug:__FILE__ line:__LINE__ message:@"R:%p C:%p ARTRealtime requesting to continue sync operation after reconnect", _realtime, self]; + ARTProtocolMessage * msg = [[ARTProtocolMessage alloc] init]; msg.action = ARTProtocolMessageSync; msg.msgSerial = self.presenceMap.syncMsgSerial; @@ -541,12 +545,12 @@ - (void)onPresence:(ARTProtocolMessage *)message { presence.id = [NSString stringWithFormat:@"%@:%d", message.id, i]; } - [self.presenceMap onceSyncEnds:^(__GENERIC(NSArray, ARTPresenceMessage *) *msgs) { - [self.presenceMap put:presence]; - [self.presenceMap clean]; - + if ([self.presenceMap add:presence]) { [self broadcastPresence:presence]; - }]; + } + if (!self.presenceMap.syncInProgress) { + [self.presenceMap clean]; + } ++i; } @@ -561,8 +565,9 @@ - (void)onSync:(ARTProtocolMessage *)message { for (int i=0; i<[message.presence count]; i++) { ARTPresenceMessage *presence = [message.presence objectAtIndex:i]; - [self.presenceMap put:presence]; - [self broadcastPresence:presence]; + if ([self.presenceMap add:presence]) { + [self broadcastPresence:presence]; + } } if ([self isLastChannelSerial:message.channelSerial]) { From 2c048ffd1c67193f3f29514b6038553c5e19c20a Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Mon, 23 Jan 2017 23:57:58 +0000 Subject: [PATCH 15/16] Fix RTP2 --- Spec/RealtimeClientChannel.swift | 8 ++-- Spec/RealtimeClientPresence.swift | 70 +++++++++++++++++++++---------- Tests/ARTRealtimePresenceTest.m | 4 +- 3 files changed, 53 insertions(+), 29 deletions(-) diff --git a/Spec/RealtimeClientChannel.swift b/Spec/RealtimeClientChannel.swift index bea897f93..f5bf6f794 100644 --- a/Spec/RealtimeClientChannel.swift +++ b/Spec/RealtimeClientChannel.swift @@ -19,7 +19,7 @@ class RealtimeClientChannel: QuickSpec { it("should process all incoming messages and presence messages as soon as a Channel becomes attached") { let options = AblyTests.commonAppSetup() let client1 = AblyTests.newRealtime(options) - defer { client1.close() } + defer { client1.dispose(); client1.close() } let channel1 = client1.channels.get("room") waitUntil(timeout: testTimeout) { done in @@ -31,7 +31,7 @@ class RealtimeClientChannel: QuickSpec { options.clientId = "Client 2" let client2 = AblyTests.newRealtime(options) - defer { client2.close() } + defer { client2.dispose(); client2.close() } let channel2 = client2.channels.get(channel1.name) channel2.subscribe("Client 1") { message in @@ -68,12 +68,12 @@ class RealtimeClientChannel: QuickSpec { expect(channel1.presenceMap.members).toEventually(haveCount(2), timeout: testTimeout) expect(channel1.presenceMap.members).to(allKeysPass({ $0.hasPrefix("Client") })) - expect(channel1.presenceMap.members).to(allValuesPass({ $0.action == .Enter })) + expect(channel1.presenceMap.members).to(allValuesPass({ $0.action == .Present })) expect(channel2.presenceMap.members).toEventually(haveCount(2), timeout: testTimeout) expect(channel2.presenceMap.members).to(allKeysPass({ $0.hasPrefix("Client") })) expect(channel2.presenceMap.members["Client 1"]!.action).to(equal(ARTPresenceAction.Present)) - expect(channel2.presenceMap.members["Client 2"]!.action).to(equal(ARTPresenceAction.Enter)) + expect(channel2.presenceMap.members["Client 2"]!.action).to(equal(ARTPresenceAction.Present)) } // RTL2 diff --git a/Spec/RealtimeClientPresence.swift b/Spec/RealtimeClientPresence.swift index 211b0feb2..9ce632373 100644 --- a/Spec/RealtimeClientPresence.swift +++ b/Spec/RealtimeClientPresence.swift @@ -895,7 +895,7 @@ class RealtimeClientPresence: QuickSpec { } var user50PresentTimestamp: NSDate? - channel.presenceMap.testSuite_getArgumentFrom(#selector(ARTPresenceMap.put(_:)), atIndex: 0) { arg0 in + channel.presenceMap.testSuite_getArgumentFrom(#selector(ARTPresenceMap.add(_:)), atIndex: 0) { arg0 in let member = arg0 as! ARTPresenceMessage if member.clientId == "user50" && member.action == .Present { user50PresentTimestamp = member.timestamp @@ -903,8 +903,11 @@ class RealtimeClientPresence: QuickSpec { } waitUntil(timeout: testTimeout) { done in - channel.attach() { _ in - let transport = client.transport as! TestProxyTransport + channel.attach() { error in + expect(error).to(beNil()) + guard let transport = client.transport as? TestProxyTransport else { + fail("Transport is nil"); done(); return + } transport.beforeProcessingReceivedMessage = { protocolMessage in // A leave event for a member can arrive before that member is later registered as present as part of the initial SYNC operation. if protocolMessage.action == .Sync { @@ -914,15 +917,20 @@ class RealtimeClientPresence: QuickSpec { client.onChannelMessage(msg) done() } + transport.beforeProcessingReceivedMessage = nil } } } + channel.presence.unsubscribe() waitUntil(timeout: testTimeout) { done in channel.presence.get { members, error in expect(error).to(beNil()) - expect(members).to(haveCount(99)) - expect(members!.filter{ $0.clientId == "user50" }).to(haveCount(0)) + guard let members = members else { + fail("Members is nil"); done(); return + } + expect(members.count) == 99 + expect(members.filter{ $0.clientId == "user50" }).to(haveCount(0)) done() } } @@ -951,10 +959,10 @@ class RealtimeClientPresence: QuickSpec { fail("Missing Presence message"); return } - expect(intialPresenceMessage.memberKey()).to(equal("\(client.connection.id):tester")) + expect(intialPresenceMessage.memberKey()).to(equal("\(client.connection.id!):tester")) var compareForNewnessMethodCalls = 0 - let hook = channel.presenceMap.testSuite_injectIntoMethodAfter(NSSelectorFromString("compareForNewness")) { + let hook = channel.presenceMap.testSuite_injectIntoMethodAfter(NSSelectorFromString("isNewestPresence:comparingWith:")) { compareForNewnessMethodCalls += 1 } defer { hook.remove() } @@ -990,7 +998,7 @@ class RealtimeClientPresence: QuickSpec { waitUntil(timeout: testTimeout) { done in clientMembers = AblyTests.addMembersSequentiallyToChannel("foo", members: 100, options: options) { done() - }.first + } } let clientSubscribed = AblyTests.newRealtime(options) @@ -1073,7 +1081,7 @@ class RealtimeClientPresence: QuickSpec { waitUntil(timeout: testTimeout) { done in clientMembers = AblyTests.addMembersSequentiallyToChannel("foo", members: 100, options: options) { done() - }.first + } } let clientSubscribed = AblyTests.newRealtime(options) @@ -1159,7 +1167,7 @@ class RealtimeClientPresence: QuickSpec { waitUntil(timeout: testTimeout) { done in clientMembers = AblyTests.addMembersSequentiallyToChannel("foo", members: 120, options: options) { done() - }.first + } } guard let membersConnectionId = clientMembers?.connection.id else { fail("Members client isn't connected"); return @@ -1213,7 +1221,7 @@ class RealtimeClientPresence: QuickSpec { waitUntil(timeout: testTimeout) { done in clientMembers = AblyTests.addMembersSequentiallyToChannel("foo", members: 120, options: options) { done() - }.first + } } guard let membersConnectionId = clientMembers?.connection.id else { fail("Members client isn't connected"); return @@ -1298,6 +1306,10 @@ class RealtimeClientPresence: QuickSpec { expect(error).to(beNil()) partialDone() } + channel.presence.updateClient("tester", data: nil) { error in + expect(error).to(beNil()) + partialDone() + } } expect(channel.presenceMap.members).to(haveCount(1)) @@ -1314,7 +1326,7 @@ class RealtimeClientPresence: QuickSpec { waitUntil(timeout: testTimeout) { done in clientMembers = AblyTests.addMembersSequentiallyToChannel("foo", members: 1, options: options) { done() - }.first + } } let client = ARTRealtime(options: options) @@ -1346,7 +1358,7 @@ class RealtimeClientPresence: QuickSpec { waitUntil(timeout: testTimeout) { done in clientMembers = AblyTests.addMembersSequentiallyToChannel("foo", members: 20, options: options) { done() - }.first + } } let client = AblyTests.newRealtime(options) @@ -1391,7 +1403,7 @@ class RealtimeClientPresence: QuickSpec { waitUntil(timeout: testTimeout) { done in clientMembers = AblyTests.addMembersSequentiallyToChannel("foo", members: 20, options: options) { done() - }.first + } } let client = AblyTests.newRealtime(options) @@ -1407,25 +1419,37 @@ class RealtimeClientPresence: QuickSpec { let partialDone = AblyTests.splitDone(3, done: done) channel.presenceMap.testSuite_injectIntoMethodAfter(#selector(ARTPresenceMap.startSync)) { expect(channel.presenceMap.syncInProgress).to(beTrue()) - clientMembers?.channels.get("foo").presence.leaveClient("user11", data: nil) { _ in + + channel.presence.subscribe(.Leave) { leave in + expect(leave.clientId).to(equal("user11")) + expect(channel.presenceMap.members.filter{ _, presence in presence.action == .Leave }).to(beEmpty()) + expect(channel.presenceMap.members.filter{ _, presence in presence.action == .Absent }).to(haveCount(1)) partialDone() } - transport.afterProcessingReceivedMessage = { protocolMessage in - if protocolMessage.action == .Presence && protocolMessage.presence?[0].action == .Some(.Leave) { - expect(protocolMessage.presence).to(haveCount(1)) - partialDone() - } - } + + // Inject a fabricated Presence message + let leaveMessage = ARTProtocolMessage() + leaveMessage.action = .Presence + leaveMessage.channel = channel.name + leaveMessage.connectionSerial = client.connection.serial + 1 + leaveMessage.timestamp = NSDate() + leaveMessage.presence = [ + ARTPresenceMessage(clientId: "user11", action: .Leave, connectionId: "another", id: "another:123:0", timestamp: NSDate()) + ] + transport.receive(leaveMessage) + } + channel.presenceMap.testSuite_injectIntoMethodBefore(#selector(ARTPresenceMap.endSync)) { + expect(channel.presenceMap.members.filter{ _, presence in presence.action == .Absent }).to(haveCount(1)) + partialDone() } channel.presenceMap.testSuite_injectIntoMethodAfter(#selector(ARTPresenceMap.endSync)) { expect(channel.presenceMap.syncInProgress).to(beFalse()) + expect(channel.presenceMap.members.filter{ _, presence in presence.action == .Leave }).to(beEmpty()) expect(channel.presenceMap.members.filter{ _, presence in presence.action == .Absent }).to(beEmpty()) partialDone() } } - expect(channel.presenceMap.members.filter{ _, presence in presence.action == .Absent }).to(haveCount(1)) - expect(channel.presenceMap.members).to(haveCount(19)) } diff --git a/Tests/ARTRealtimePresenceTest.m b/Tests/ARTRealtimePresenceTest.m index dd762565f..06a1480a3 100644 --- a/Tests/ARTRealtimePresenceTest.m +++ b/Tests/ARTRealtimePresenceTest.m @@ -483,7 +483,7 @@ - (void)testEnterAndGet { XCTAssert(!error); XCTAssertEqual(2, members.count); XCTAssertEqual(members[0].action, ARTPresencePresent); - XCTAssertEqual(members[1].action, ARTPresenceEnter); + XCTAssertEqual(members[1].action, ARTPresencePresent); XCTAssertEqualObjects([members[0] data], enterData); XCTAssertEqualObjects([members[1] data], enterData); [expectation fulfill]; @@ -966,7 +966,7 @@ - (void)testPresenceWithData { [channel.presence get:^(NSArray *members, ARTErrorInfo *error) { XCTAssert(!error); XCTAssertEqual(1, members.count); - XCTAssertEqual(members[0].action, ARTPresenceEnter); + XCTAssertEqual(members[0].action, ARTPresencePresent); XCTAssertEqualObjects(members[0].clientId, [self getClientId]); XCTAssertEqualObjects([members[0] data], @"someDataPayload"); [expectation fulfill]; From 5f047e6cc703cd02335477aecc7c3fb49e03c2e4 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Tue, 24 Jan 2017 15:25:15 +0000 Subject: [PATCH 16/16] Remove warnings --- Tests/ARTRealtimeRecoverTest.m | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/ARTRealtimeRecoverTest.m b/Tests/ARTRealtimeRecoverTest.m index abcaf909c..24ab92b56 100644 --- a/Tests/ARTRealtimeRecoverTest.m +++ b/Tests/ARTRealtimeRecoverTest.m @@ -36,7 +36,7 @@ - (void)testRecoverDisconnected { __weak XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"%s", __FUNCTION__]]; ARTRealtime *realtime = [[ARTRealtime alloc] initWithOptions:options]; __block NSString *firstConnectionId = nil; - [realtime.connection once:ARTRealtimeConnected callback:^(ARTConnectionStateChange *stateChange) { + [realtime.connection once:ARTRealtimeConnectionEventConnected callback:^(ARTConnectionStateChange *stateChange) { firstConnectionId = realtime.connection.id; ARTRealtimeChannel *channel = [realtime.channels get:channelName]; // Sending a message @@ -48,7 +48,7 @@ - (void)testRecoverDisconnected { [self waitForExpectationsWithTimeout:[ARTTestUtil timeout] handler:nil]; __weak XCTestExpectation *expectation2 = [self expectationWithDescription:[NSString stringWithFormat:@"%s-2", __FUNCTION__]]; - [realtime.connection once:ARTRealtimeDisconnected callback:^(ARTConnectionStateChange *stateChange) { + [realtime.connection once:ARTRealtimeConnectionEventDisconnected callback:^(ARTConnectionStateChange *stateChange) { options.recover = nil; ARTRealtime *realtimeNonRecovered = [[ARTRealtime alloc] initWithOptions:options]; ARTRealtimeChannel *c2 = [realtimeNonRecovered.channels get:channelName]; @@ -71,7 +71,7 @@ - (void)testRecoverDisconnected { XCTAssertEqualObjects(c2Message, [message data]); [expectation3 fulfill]; }]; - [realtimeRecovered.connection once:ARTRealtimeConnected callback:^(ARTConnectionStateChange *stateChange) { + [realtimeRecovered.connection once:ARTRealtimeConnectionEventConnected callback:^(ARTConnectionStateChange *stateChange) { XCTAssertEqualObjects(realtimeRecovered.connection.id, firstConnectionId); }]; [self waitForExpectationsWithTimeout:[ARTTestUtil timeout] handler:nil];