Skip to content

Commit 2ab7b59

Browse files
committed
[ECO-5426] Added unit tests for ObjectsManager in ObjectsManagerTest
1 parent 44d60ce commit 2ab7b59

3 files changed

Lines changed: 249 additions & 19 deletions

File tree

live-objects/src/test/kotlin/io/ably/lib/objects/unit/objects/DefaultLiveObjectsTest.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ class DefaultLiveObjectsTest {
5151
// Set up some objects in objectPool that should be cleared
5252
val rootObject = defaultLiveObjects.objectsPool.get(ROOT_OBJECT_ID) as DefaultLiveMap
5353
rootObject.data["key1"] = LiveMapEntry(data = ObjectData("testValue1"))
54-
defaultLiveObjects.objectsPool.set("dummyObjectId", DefaultLiveCounter("dummyObjectId", mockk(relaxed = true)))
54+
defaultLiveObjects.objectsPool.set("counter:testObject@1", DefaultLiveCounter("counter:testObject@1", mockk(relaxed = true)))
5555
assertEquals(2, defaultLiveObjects.objectsPool.size(), "RTO4b - Should have 2 objects before state change")
5656

5757
// RTO4b - If the HAS_OBJECTS flag is 0, the sync sequence must be considered complete immediately
@@ -102,7 +102,7 @@ class DefaultLiveObjectsTest {
102102
connectionId = "testConnectionId",
103103
operation = ObjectOperation(
104104
action = ObjectOperationAction.CounterInc,
105-
objectId = "testObjectId",
105+
objectId = "counter:testObject@1",
106106
counterOp = ObjectCounterOp(amount = 5.0)
107107
),
108108
serial = "serial1",
@@ -130,7 +130,7 @@ class DefaultLiveObjectsTest {
130130
timestamp = 1234567890L,
131131
connectionId = "testSyncConnectionId",
132132
objectState = ObjectState(
133-
objectId = "testObjectId",
133+
objectId = "map:testObject@1",
134134
tombstone = false,
135135
siteTimeserials = mapOf("site1" to "syncSerial1"),
136136
),
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
package io.ably.lib.objects.unit.objects
2+
3+
import io.ably.lib.objects.*
4+
import io.ably.lib.objects.ObjectMessage
5+
import io.ably.lib.objects.ObjectState
6+
import io.ably.lib.objects.ObjectsState
7+
import io.ably.lib.objects.type.livecounter.DefaultLiveCounter
8+
import io.ably.lib.objects.type.livemap.DefaultLiveMap
9+
import io.ably.lib.objects.unit.*
10+
import io.ably.lib.objects.unit.getDefaultLiveObjectsWithMockedDeps
11+
import io.mockk.*
12+
import org.junit.Test
13+
import kotlin.test.*
14+
15+
class ObjectsManagerTest {
16+
17+
@Test
18+
fun `(RTO5) ObjectsManager should handle object sync messages`() {
19+
val defaultLiveObjects = getDefaultLiveObjectsWithMockedDeps()
20+
assertEquals(ObjectsState.INITIALIZED, defaultLiveObjects.state, "Initial state should be INITIALIZED")
21+
22+
val objectsManager = defaultLiveObjects.ObjectsManager
23+
24+
mockZeroValuedObjects()
25+
26+
// Populate objectsPool with existing objects
27+
val objectsPool = defaultLiveObjects.ObjectsPool
28+
objectsPool.set("map:testObject@1", mockk<DefaultLiveMap>(relaxed = true))
29+
objectsPool.set("counter:testObject@4", mockk<DefaultLiveCounter>(relaxed = true))
30+
31+
// Incoming object messages
32+
val objectMessage1 = ObjectMessage(
33+
id = "testId1",
34+
objectState = ObjectState(
35+
objectId = "map:testObject@1", // already exists in pool
36+
tombstone = false,
37+
siteTimeserials = mapOf("site1" to "syncSerial1"),
38+
map = ObjectMap(),
39+
)
40+
)
41+
val objectMessage2 = ObjectMessage(
42+
id = "testId2",
43+
objectState = ObjectState(
44+
objectId = "counter:testObject@2", // Does not exist in pool
45+
tombstone = false,
46+
siteTimeserials = mapOf("site1" to "syncSerial1"),
47+
counter = ObjectCounter(count = 20.0)
48+
)
49+
)
50+
val objectMessage3 = ObjectMessage(
51+
id = "testId3",
52+
objectState = ObjectState(
53+
objectId = "map:testObject@3", // Does not exist in pool
54+
tombstone = false,
55+
siteTimeserials = mapOf("site1" to "syncSerial1"),
56+
map = ObjectMap(),
57+
)
58+
)
59+
// Should start and end sync, apply object states, and create new objects for missing ones
60+
objectsManager.handleObjectSyncMessages(listOf(objectMessage1, objectMessage2, objectMessage3), "sync-123:")
61+
62+
verify(exactly = 1) {
63+
objectsManager.startNewSync("sync-123")
64+
}
65+
verify(exactly = 1) {
66+
objectsManager.endSync(true) // deferStateEvent = true since new sync was started
67+
}
68+
val newlyCreatedObjects = mutableListOf<ObjectState>()
69+
verify(exactly = 2) {
70+
objectsManager["createObjectFromState"](capture(newlyCreatedObjects))
71+
}
72+
assertEquals("counter:testObject@2", newlyCreatedObjects[0].objectId)
73+
assertEquals("map:testObject@3", newlyCreatedObjects[1].objectId)
74+
75+
assertEquals(ObjectsState.SYNCED, defaultLiveObjects.state, "State should be SYNCED after sync sequence")
76+
// After sync `counter:testObject@4` will be removed from pool
77+
assertNull(objectsPool.get("counter:testObject@4"))
78+
assertEquals(4, objectsPool.size(), "Objects pool should contain 4 objects after sync including root")
79+
assertNotNull(objectsPool.get(ROOT_OBJECT_ID), "Root object should still exist in pool")
80+
val testObject1 = objectsPool.get("map:testObject@1")
81+
assertNotNull(testObject1, "map:testObject@1 should exist in pool after sync")
82+
verify(exactly = 1) {
83+
testObject1.applyObjectSync(any<ObjectState>())
84+
}
85+
val testObject2 = objectsPool.get("counter:testObject@2")
86+
assertNotNull(testObject2, "counter:testObject@2 should exist in pool after sync")
87+
verify(exactly = 1) {
88+
testObject2.applyObjectSync(any<ObjectState>())
89+
}
90+
val testObject3 = objectsPool.get("map:testObject@3")
91+
assertNotNull(testObject3, "map:testObject@3 should exist in pool after sync")
92+
verify(exactly = 1) {
93+
testObject3.applyObjectSync(any<ObjectState>())
94+
}
95+
}
96+
97+
@Test
98+
fun `(RTO8) ObjectsManager should apply object operation when state is synced`() {
99+
val defaultLiveObjects = getDefaultLiveObjectsWithMockedDeps()
100+
defaultLiveObjects.state = ObjectsState.SYNCED // Ensure we're in SYNCED state
101+
102+
val objectsManager = defaultLiveObjects.ObjectsManager
103+
104+
mockZeroValuedObjects()
105+
106+
// Populate objectsPool with existing objects
107+
val objectsPool = defaultLiveObjects.ObjectsPool
108+
objectsPool.set("map:testObject@1", mockk<DefaultLiveMap>(relaxed = true))
109+
110+
// Incoming object messages with operation field instead of objectState
111+
val objectMessage1 = ObjectMessage(
112+
id = "testId1",
113+
operation = ObjectOperation(
114+
action = ObjectOperationAction.MapSet, // Assuming this is the right action for maps
115+
objectId = "map:testObject@1", // already exists in pool
116+
),
117+
serial = "serial1",
118+
siteCode = "site1"
119+
)
120+
121+
val objectMessage2 = ObjectMessage(
122+
id = "testId2",
123+
operation = ObjectOperation(
124+
action = ObjectOperationAction.CounterCreate, // Set the counter value
125+
objectId = "counter:testObject@2", // Does not exist in pool
126+
),
127+
serial = "serial2",
128+
siteCode = "site1"
129+
)
130+
131+
val objectMessage3 = ObjectMessage(
132+
id = "testId3",
133+
operation = ObjectOperation(
134+
action = ObjectOperationAction.MapCreate,
135+
objectId = "map:testObject@3", // Does not exist in pool
136+
),
137+
serial = "serial3",
138+
siteCode = "site1"
139+
)
140+
141+
// RTO8b - Apply messages immediately if synced
142+
objectsManager.handleObjectMessages(listOf(objectMessage1, objectMessage2, objectMessage3))
143+
assertEquals(0, objectsManager.BufferedObjectOperations.size, "No buffer needed in SYNCED state")
144+
145+
assertEquals(4, objectsPool.size(), "Objects pool should contain 4 objects including root")
146+
assertNotNull(objectsPool.get(ROOT_OBJECT_ID), "Root object should still exist in pool")
147+
148+
val testObject1 = objectsPool.get("map:testObject@1")
149+
assertNotNull(testObject1, "map:testObject@1 should exist in pool after sync")
150+
verify(exactly = 1) {
151+
testObject1.applyObject(objectMessage1)
152+
}
153+
val testObject2 = objectsPool.get("counter:testObject@2")
154+
assertNotNull(testObject2, "counter:testObject@2 should exist in pool after sync")
155+
verify(exactly = 1) {
156+
testObject2.applyObject(objectMessage2)
157+
}
158+
val testObject3 = objectsPool.get("map:testObject@3")
159+
assertNotNull(testObject3, "map:testObject@3 should exist in pool after sync")
160+
verify(exactly = 1) {
161+
testObject3.applyObject(objectMessage3)
162+
}
163+
}
164+
165+
@Test
166+
fun `(RTO7) ObjectsManager should buffer operations during sync, apply them after synced`() {
167+
val defaultLiveObjects = getDefaultLiveObjectsWithMockedDeps()
168+
val objectsManager = defaultLiveObjects.ObjectsManager
169+
assertEquals(0, objectsManager.BufferedObjectOperations.size, "RTO7a1 - Initial buffer should be empty")
170+
171+
val objectsPool = defaultLiveObjects.ObjectsPool
172+
assertEquals(1, objectsPool.size(), "RTO7a2 - Initial pool should contain only root object")
173+
174+
mockZeroValuedObjects()
175+
176+
// Set state to SYNCING
177+
defaultLiveObjects.state = ObjectsState.SYNCING
178+
179+
val objectMessage = ObjectMessage(
180+
id = "testId",
181+
operation = ObjectOperation(
182+
action = ObjectOperationAction.CounterCreate,
183+
objectId = "counter:testObject@1",
184+
counterOp = ObjectCounterOp(amount = 5.30)
185+
),
186+
serial = "serial1",
187+
siteCode = "site1"
188+
)
189+
190+
// RTO7a - Buffer operations during sync
191+
objectsManager.handleObjectMessages(listOf(objectMessage))
192+
193+
verify(exactly = 0) {
194+
objectsManager["applyObjectMessages"](any<List<ObjectMessage>>())
195+
}
196+
assertEquals(1, objectsManager.BufferedObjectOperations.size)
197+
assertEquals(objectMessage, objectsManager.BufferedObjectOperations[0])
198+
assertEquals(1, objectsPool.size(), "Pool should still contain only root object during sync")
199+
200+
// RTO7 - Apply buffered operations after sync
201+
objectsManager.endSync(false) // End sync without new sync
202+
verify(exactly = 1) {
203+
objectsManager["applyObjectMessages"](any<List<ObjectMessage>>())
204+
}
205+
assertEquals(0, objectsManager.BufferedObjectOperations.size)
206+
assertEquals(2, objectsPool.size(), "Pool should contain 2 objects after applying buffered operations")
207+
assertNotNull(objectsPool.get("counter:testObject@1"), "Counter object should be created after sync")
208+
assertTrue(objectsPool.get("counter:testObject@1") is DefaultLiveCounter, "Should create a DefaultLiveCounter object")
209+
}
210+
211+
private fun mockZeroValuedObjects() {
212+
mockkObject(DefaultLiveMap.Companion)
213+
every {
214+
DefaultLiveMap.zeroValue(any<String>(), any<LiveObjectsAdapter>(), any<ObjectsPool>())
215+
} answers {
216+
mockk<DefaultLiveMap>(relaxed = true)
217+
}
218+
mockkObject(DefaultLiveCounter.Companion)
219+
every {
220+
DefaultLiveCounter.zeroValue(any<String>(), any<LiveObjectsAdapter>())
221+
} answers {
222+
mockk<DefaultLiveCounter>(relaxed = true)
223+
}
224+
}
225+
226+
@AfterTest
227+
fun tearDown() {
228+
unmockkAll() // Clean up all mockk objects after each test
229+
}
230+
}

live-objects/src/test/kotlin/io/ably/lib/objects/unit/objects/ObjectsPoolTest.kt

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,13 @@ class ObjectsPoolTest {
3232
assertEquals(1, objectsPool.size(), "RTO3 - Should only contain the root object initially")
3333

3434
// RTO3a - ObjectsPool is a Dict, a map of LiveObjects keyed by objectId string
35-
val testLiveMap = DefaultLiveMap("testObjectId", mockk(relaxed = true), objectsPool)
36-
objectsPool.set("testObjectId", testLiveMap)
37-
val testLiveCounter = DefaultLiveCounter("testCounterId", mockk(relaxed = true))
38-
objectsPool.set("testCounterId", testLiveCounter)
35+
val testLiveMap = DefaultLiveMap("map:testObject@1", mockk(relaxed = true), objectsPool)
36+
objectsPool.set("map:testObject@1", testLiveMap)
37+
val testLiveCounter = DefaultLiveCounter("counter:testObject@1", mockk(relaxed = true))
38+
objectsPool.set("counter:testObject@1", testLiveCounter)
3939
// Assert that the objects are stored in the pool
40-
assertEquals(testLiveMap, objectsPool.get("testObjectId"))
41-
assertEquals(testLiveCounter, objectsPool.get("testCounterId"))
40+
assertEquals(testLiveMap, objectsPool.get("map:testObject@1"))
41+
assertEquals(testLiveCounter, objectsPool.get("counter:testObject@1"))
4242
assertEquals(3, objectsPool.size(), "RTO3 - Should have 3 objects in pool (root + testLiveMap + testLiveCounter)")
4343
}
4444

@@ -88,11 +88,11 @@ class ObjectsPoolTest {
8888
assertEquals(2, rootMap.data.size, "RTO3 - Root map should have initial data")
8989

9090
// Add some objects
91-
objectsPool.set("testObjectId", DefaultLiveCounter("testObjectId", mockk(relaxed = true)))
91+
objectsPool.set("counter:testObject@1", DefaultLiveCounter("counter:testObject@1", mockk(relaxed = true)))
9292
assertEquals(2, objectsPool.size()) // root + testObject
93-
objectsPool.set("anotherObjectId", DefaultLiveCounter("anotherObjectId", mockk(relaxed = true)))
93+
objectsPool.set("counter:testObject@2", DefaultLiveCounter("counter:testObject@2", mockk(relaxed = true)))
9494
assertEquals(3, objectsPool.size()) // root + testObject + anotherObject
95-
objectsPool.set("testMapId", DefaultLiveMap("testMapId", mockk(relaxed = true), objectsPool))
95+
objectsPool.set("map:testObject@1", DefaultLiveMap("map:testObject@1", mockk(relaxed = true), objectsPool))
9696
assertEquals(4, objectsPool.size()) // root + testObject + anotherObject + testMap
9797

9898
// Reset to initial pool
@@ -111,22 +111,22 @@ class ObjectsPoolTest {
111111
val objectsPool = defaultLiveObjects.objectsPool
112112

113113
// Add some objects
114-
objectsPool.set("object1", DefaultLiveCounter("object1", mockk(relaxed = true)))
115-
objectsPool.set("object2", DefaultLiveCounter("object2", mockk(relaxed = true)))
116-
objectsPool.set("object3", DefaultLiveCounter("object3", mockk(relaxed = true)))
114+
objectsPool.set("counter:testObject@1", DefaultLiveCounter("counter:testObject@1", mockk(relaxed = true)))
115+
objectsPool.set("counter:testObject@2", DefaultLiveCounter("counter:testObject@2", mockk(relaxed = true)))
116+
objectsPool.set("counter:testObject@3", DefaultLiveCounter("counter:testObject@3", mockk(relaxed = true)))
117117
assertEquals(4, objectsPool.size()) // root + 3 objects
118118

119119
// Delete extra object IDs (keep only object1 and object2)
120-
val receivedObjectIds = mutableSetOf("object1", "object2")
120+
val receivedObjectIds = mutableSetOf("counter:testObject@1", "counter:testObject@2")
121121
objectsPool.deleteExtraObjectIds(receivedObjectIds)
122122

123123
// Should only contain root, object1, and object2
124124
assertEquals(3, objectsPool.size())
125125
// RTO5c2a - Should keep the root object
126126
assertNotNull(objectsPool.get(ROOT_OBJECT_ID))
127127
// RTO5c2 - Should delete object3 and keep object1 and object2
128-
assertNotNull(objectsPool.get("object1"))
129-
assertNotNull(objectsPool.get("object2"))
130-
assertNull(objectsPool.get("object3")) // Should be deleted
128+
assertNotNull(objectsPool.get("counter:testObject@1"))
129+
assertNotNull(objectsPool.get("counter:testObject@2"))
130+
assertNull(objectsPool.get("counter:testObject@3")) // Should be deleted
131131
}
132132
}

0 commit comments

Comments
 (0)