Skip to content

Commit e808d38

Browse files
committed
[ECO-5426] Added missing unit tests for BaseLiveObject, refactored code
1 parent 2ab7b59 commit e808d38

10 files changed

Lines changed: 197 additions & 24 deletions

File tree

live-objects/src/main/kotlin/io/ably/lib/objects/ObjectsPool.kt

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -45,17 +45,13 @@ internal class ObjectsPool(
4545
* Coroutine scope for garbage collection
4646
*/
4747
private val gcScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
48-
49-
/**
50-
* Job for the garbage collection coroutine
51-
*/
52-
private var gcJob: Job? = null
48+
private var gcJob: Job // Job for the garbage collection coroutine
5349

5450
init {
5551
// Initialize pool with root object
5652
createInitialPool()
5753
// Start garbage collection coroutine
58-
startGCJob()
54+
gcJob = startGCJob()
5955
}
6056

6157
/**
@@ -146,8 +142,8 @@ internal class ObjectsPool(
146142
/**
147143
* Starts the garbage collection coroutine.
148144
*/
149-
private fun startGCJob() {
150-
gcJob = gcScope.launch {
145+
private fun startGCJob() : Job {
146+
return gcScope.launch {
151147
while (isActive) {
152148
try {
153149
onGCInterval()
@@ -164,7 +160,7 @@ internal class ObjectsPool(
164160
* Should be called when the pool is no longer needed.
165161
*/
166162
fun dispose() {
167-
gcJob?.cancel()
163+
gcJob.cancel()
168164
gcScope.cancel()
169165
pool.clear()
170166
}

live-objects/src/main/kotlin/io/ably/lib/objects/ObjectsSyncTracker.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ package io.ably.lib.objects
44
* @spec RTO5 - SyncTracker class for tracking objects sync status
55
*/
66
internal class ObjectsSyncTracker(syncChannelSerial: String?) {
7+
private val syncSerial: String? = syncChannelSerial
78
internal val syncId: String?
89
internal val syncCursor: String?
9-
private val syncSerial: String? = syncChannelSerial
1010

1111
init {
1212
val parsed = parseSyncChannelSerial(syncChannelSerial)

live-objects/src/main/kotlin/io/ably/lib/objects/type/BaseLiveObject.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ internal abstract class BaseLiveObject(
2929

3030
protected open val tag = "BaseLiveObject"
3131

32-
private val siteTimeserials = mutableMapOf<String, String>() // RTLO3b
32+
internal val siteTimeserials = mutableMapOf<String, String>() // RTLO3b
3333

3434
internal var createOperationIsMerged = false // RTLO3c
3535

@@ -111,7 +111,7 @@ internal abstract class BaseLiveObject(
111111
*
112112
* @spec RTLO4a - Serial comparison logic for LiveMap/LiveCounter operations
113113
*/
114-
private fun canApplyOperation(siteCode: String?, timeSerial: String?): Boolean {
114+
internal fun canApplyOperation(siteCode: String?, timeSerial: String?): Boolean {
115115
if (timeSerial.isNullOrEmpty()) {
116116
throw objectError("Invalid serial: $timeSerial") // RTLO4a3
117117
}

live-objects/src/main/kotlin/io/ably/lib/objects/type/livecounter/DefaultLiveCounter.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import io.ably.lib.types.Callback
1212
*
1313
* @spec RTLC1/RTLC2 - LiveCounter implementation extends LiveObject
1414
*/
15-
internal class DefaultLiveCounter(
15+
internal class DefaultLiveCounter private constructor(
1616
objectId: String,
1717
adapter: LiveObjectsAdapter,
1818
) : LiveCounter, BaseLiveObject(objectId, ObjectType.Counter, adapter) {

live-objects/src/main/kotlin/io/ably/lib/objects/type/livemap/DefaultLiveMap.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ private fun LiveMapEntry.isEligibleForGc(): Boolean {
3535
*
3636
* @spec RTLM1/RTLM2 - LiveMap implementation extends LiveObject
3737
*/
38-
internal class DefaultLiveMap(
38+
internal class DefaultLiveMap private constructor(
3939
objectId: String,
4040
adapter: LiveObjectsAdapter,
4141
internal val objectsPool: ObjectsPool,

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import kotlinx.coroutines.delay
66
import kotlinx.coroutines.suspendCancellableCoroutine
77
import kotlinx.coroutines.withContext
88
import kotlinx.coroutines.withTimeout
9+
import java.lang.reflect.Method
910

1011
suspend fun assertWaiter(timeoutInMs: Long = 10_000, block: suspend () -> Boolean) {
1112
withContext(Dispatchers.Default) {
@@ -58,3 +59,7 @@ fun <T> Any.invokePrivateMethod(methodName: String, vararg args: Any?): T {
5859
@Suppress("UNCHECKED_CAST")
5960
return method.invoke(this, *args) as T
6061
}
62+
63+
fun Class<*>.findMethod(methodName: String): Method {
64+
return methods.find { it.name.contains(methodName) } ?: error("Method '$methodName' not found")
65+
}

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

Lines changed: 1 addition & 1 deletion
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("counter:testObject@1", DefaultLiveCounter("counter:testObject@1", mockk(relaxed = true)))
54+
defaultLiveObjects.objectsPool.set("counter:testObject@1", DefaultLiveCounter.zeroValue("counter:testObject@1", mockk()))
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

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,8 +163,10 @@ class ObjectsManagerTest {
163163
}
164164

165165
@Test
166-
fun `(RTO7) ObjectsManager should buffer operations during sync, apply them after synced`() {
166+
fun `(RTO7) ObjectsManager should buffer operations when not in sync, apply them after synced`() {
167167
val defaultLiveObjects = getDefaultLiveObjectsWithMockedDeps()
168+
assertEquals(ObjectsState.INITIALIZED, defaultLiveObjects.state, "Initial state should be INITIALIZED")
169+
168170
val objectsManager = defaultLiveObjects.ObjectsManager
169171
assertEquals(0, objectsManager.BufferedObjectOperations.size, "RTO7a1 - Initial buffer should be empty")
170172

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

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@ 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("map:testObject@1", mockk(relaxed = true), objectsPool)
35+
val testLiveMap = DefaultLiveMap.zeroValue("map:testObject@1", mockk(relaxed = true), objectsPool)
3636
objectsPool.set("map:testObject@1", testLiveMap)
37-
val testLiveCounter = DefaultLiveCounter("counter:testObject@1", mockk(relaxed = true))
37+
val testLiveCounter = DefaultLiveCounter.zeroValue("counter:testObject@1", mockk(relaxed = true))
3838
objectsPool.set("counter:testObject@1", testLiveCounter)
3939
// Assert that the objects are stored in the pool
4040
assertEquals(testLiveMap, objectsPool.get("map:testObject@1"))
@@ -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("counter:testObject@1", DefaultLiveCounter("counter:testObject@1", mockk(relaxed = true)))
91+
objectsPool.set("counter:testObject@1", DefaultLiveCounter.zeroValue("counter:testObject@1", mockk(relaxed = true)))
9292
assertEquals(2, objectsPool.size()) // root + testObject
93-
objectsPool.set("counter:testObject@2", DefaultLiveCounter("counter:testObject@2", mockk(relaxed = true)))
93+
objectsPool.set("counter:testObject@2", DefaultLiveCounter.zeroValue("counter:testObject@2", mockk(relaxed = true)))
9494
assertEquals(3, objectsPool.size()) // root + testObject + anotherObject
95-
objectsPool.set("map:testObject@1", DefaultLiveMap("map:testObject@1", mockk(relaxed = true), objectsPool))
95+
objectsPool.set("map:testObject@1", DefaultLiveMap.zeroValue("map:testObject@1", mockk(relaxed = true), objectsPool))
9696
assertEquals(4, objectsPool.size()) // root + testObject + anotherObject + testMap
9797

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

113113
// Add some objects
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)))
114+
objectsPool.set("counter:testObject@1", DefaultLiveCounter.zeroValue("counter:testObject@1", mockk(relaxed = true)))
115+
objectsPool.set("counter:testObject@2", DefaultLiveCounter.zeroValue("counter:testObject@2", mockk(relaxed = true)))
116+
objectsPool.set("counter:testObject@3", DefaultLiveCounter.zeroValue("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)
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
package io.ably.lib.objects.unit.type
2+
3+
import io.ably.lib.objects.*
4+
import io.ably.lib.objects.type.BaseLiveObject
5+
import io.ably.lib.objects.type.livecounter.DefaultLiveCounter
6+
import io.ably.lib.objects.type.livemap.DefaultLiveMap
7+
import io.mockk.mockk
8+
import org.junit.Test
9+
import kotlin.test.assertEquals
10+
import kotlin.test.assertFalse
11+
import kotlin.test.assertTrue
12+
import kotlin.test.assertFailsWith
13+
14+
class BaseLiveObjectTest {
15+
16+
@Test
17+
fun `(RTLO1, RTLO2) BaseLiveObject should be abstract base class for LiveMap and LiveCounter`() {
18+
// RTLO2 - Check that BaseLiveObject is abstract
19+
val isAbstract = java.lang.reflect.Modifier.isAbstract(BaseLiveObject::class.java.modifiers)
20+
assertTrue(isAbstract, "BaseLiveObject should be an abstract class")
21+
22+
// RTLO1 - Check that BaseLiveObject is the parent class of DefaultLiveMap and DefaultLiveCounter
23+
assertTrue(BaseLiveObject::class.java.isAssignableFrom(DefaultLiveMap::class.java),
24+
"DefaultLiveMap should extend BaseLiveObject")
25+
assertTrue(BaseLiveObject::class.java.isAssignableFrom(DefaultLiveCounter::class.java),
26+
"DefaultLiveCounter should extend BaseLiveObject")
27+
}
28+
29+
@Test
30+
fun `(RTLO3) BaseLiveObject should have required properties`() {
31+
val liveMap: BaseLiveObject = DefaultLiveMap.zeroValue("map:testObject@1", mockk(), mockk())
32+
val liveCounter: BaseLiveObject = DefaultLiveCounter.zeroValue("counter:testObject@1", mockk())
33+
// RTLO3a - check that objectId is set correctly
34+
assertEquals("map:testObject@1", liveMap.objectId)
35+
assertEquals("counter:testObject@1", liveCounter.objectId)
36+
37+
// RTLO3b, RTLO3b1 - check that siteTimeserials is initialized as an empty map
38+
assertEquals(emptyMap(), liveMap.siteTimeserials)
39+
assertEquals(emptyMap(), liveCounter.siteTimeserials)
40+
41+
// RTLO3c - Create operation merged flag
42+
assertFalse(liveMap.createOperationIsMerged, "Create operation should not be merged by default")
43+
assertFalse(liveCounter.createOperationIsMerged, "Create operation should not be merged by default")
44+
}
45+
46+
@Test
47+
fun `(RTLO4a1, RTLO4a2) canApplyOperation should accept ObjectMessage params and return boolean`() {
48+
// RTLO4a1a - Assert parameter types and return type based on method signature using reflection
49+
val method = BaseLiveObject::class.java.findMethod("canApplyOperation")
50+
51+
// RTLO4a1a - Verify parameter types
52+
val parameters = method.parameters
53+
assertEquals(2, parameters.size, "canApplyOperation should have exactly 2 parameters")
54+
55+
// First parameter should be String? (siteCode)
56+
assertEquals(String::class.java, parameters[0].type, "First parameter should be of type String?")
57+
assertTrue(parameters[0].isVarArgs.not(), "First parameter should not be varargs")
58+
59+
// Second parameter should be String? (timeSerial)
60+
assertEquals(String::class.java, parameters[1].type, "Second parameter should be of type String?")
61+
assertTrue(parameters[1].isVarArgs.not(), "Second parameter should not be varargs")
62+
63+
// RTLO4a2 - Verify return type
64+
assertEquals(Boolean::class.java, method.returnType, "canApplyOperation should return Boolean")
65+
}
66+
67+
@Test
68+
fun `(RTLO4a3) canApplyOperation should throw error for null or empty incoming siteSerial`() {
69+
val liveMap: BaseLiveObject = DefaultLiveMap.zeroValue("map:testObject@1", mockk(), mockk())
70+
71+
// Test null serial
72+
assertFailsWith<Exception>("Should throw error for null serial") {
73+
liveMap.canApplyOperation("site1", null)
74+
}
75+
76+
// Test empty serial
77+
assertFailsWith<Exception>("Should throw error for empty serial") {
78+
liveMap.canApplyOperation("site1", "")
79+
}
80+
81+
// Test null siteCode
82+
assertFailsWith<Exception>("Should throw error for null site code") {
83+
liveMap.canApplyOperation(null, "serial1")
84+
}
85+
86+
// Test empty siteCode
87+
assertFailsWith<Exception>("Should throw error for empty site code") {
88+
liveMap.canApplyOperation("", "serial1")
89+
}
90+
}
91+
92+
@Test
93+
fun `(RTLO4a4, RTLO4a5) canApplyOperation should return true when existing siteSerial is null or empty`() {
94+
val liveMap: BaseLiveObject = DefaultLiveMap.zeroValue("map:testObject@1", mockk(), mockk())
95+
assertTrue(liveMap.siteTimeserials.isEmpty(), "Initial siteTimeserials should be empty")
96+
97+
// RTLO4a4 - Get siteSerial from siteTimeserials map
98+
// RTLO4a5 - Return true when siteSerial is null (no entry in map)
99+
assertTrue(liveMap.canApplyOperation("site1", "serial1"),
100+
"Should return true when no siteSerial exists for the site")
101+
102+
// RTLO4a5 - Return true when siteSerial is empty string
103+
liveMap.siteTimeserials["site1"] = ""
104+
assertTrue(liveMap.canApplyOperation("site1", "serial1"),
105+
"Should return true when siteSerial is empty string")
106+
}
107+
108+
@Test
109+
fun `(RTLO4a6) canApplyOperation should return true when message siteSerial is greater than existing siteSerial`() {
110+
val liveMap: BaseLiveObject = DefaultLiveMap.zeroValue("map:testObject@1", mockk(), mockk())
111+
112+
// Set existing siteSerial
113+
liveMap.siteTimeserials["site1"] = "serial1"
114+
115+
// RTLO4a6 - Return true when message serial is greater (lexicographically)
116+
assertTrue(liveMap.canApplyOperation("site1", "serial2"),
117+
"Should return true when message serial 'serial2' > siteSerial 'serial1'")
118+
119+
assertTrue(liveMap.canApplyOperation("site1", "serial10"),
120+
"Should return true when message serial 'serial10' > siteSerial 'serial1'")
121+
122+
assertTrue(liveMap.canApplyOperation("site1", "serialA"),
123+
"Should return true when message serial 'serialA' > siteSerial 'serial1'")
124+
}
125+
126+
@Test
127+
fun `(RTLO4a6) canApplyOperation should return false when message siteSerial is less than or equal to siteSerial`() {
128+
val liveMap: BaseLiveObject = DefaultLiveMap.zeroValue("map:testObject@1", mockk(), mockk())
129+
130+
// Set existing siteSerial
131+
liveMap.siteTimeserials["site1"] = "serial2"
132+
133+
// RTLO4a6 - Return false when message serial is less than siteSerial
134+
assertFalse(liveMap.canApplyOperation("site1", "serial1"),
135+
"Should return false when message serial 'serial1' < siteSerial 'serial2'")
136+
137+
// RTLO4a6 - Return false when message serial equals siteSerial
138+
assertFalse(liveMap.canApplyOperation("site1", "serial2"),
139+
"Should return false when message serial equals siteSerial")
140+
141+
// RTLO4a6 - Return false when message serial is less (lexicographically)
142+
assertTrue(liveMap.canApplyOperation("site1", "serialA"),
143+
"Should return false when message serial 'serialA' < siteSerial 'serial2'")
144+
}
145+
146+
@Test
147+
fun `(RTLO4a) canApplyOperation should work with different site codes`() {
148+
val liveMap: BaseLiveObject = DefaultLiveCounter.zeroValue("map:testObject@1", mockk())
149+
150+
// Set serials for different sites
151+
liveMap.siteTimeserials["site1"] = "serial1"
152+
liveMap.siteTimeserials["site2"] = "serial5"
153+
154+
// Test site1
155+
assertTrue(liveMap.canApplyOperation("site1", "serial2"),
156+
"Should return true for site1 when serial2 > serial1")
157+
assertFalse(liveMap.canApplyOperation("site1", "serial1"),
158+
"Should return false for site1 when serial1 = serial1")
159+
160+
// Test site2
161+
assertTrue(liveMap.canApplyOperation("site2", "serial6"),
162+
"Should return true for site2 when serial6 > serial5")
163+
assertFalse(liveMap.canApplyOperation("site2", "serial4"),
164+
"Should return false for site2 when serial4 < serial5")
165+
166+
// Test new site (should return true)
167+
assertTrue(liveMap.canApplyOperation("site3", "serial1"),
168+
"Should return true for new site with any serial")
169+
}
170+
}

0 commit comments

Comments
 (0)