Skip to content

Commit 6c8a793

Browse files
committed
[ECO-5380] Create separated unit test file for ObjectMessage
1. Added three different unit tests covering edge cases 2. Updated doc for byteSize String extension method
1 parent b0374ad commit 6c8a793

3 files changed

Lines changed: 176 additions & 38 deletions

File tree

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ internal fun serverError(errorMessage: String) = ablyException(errorMessage, Err
3737
/**
3838
* Calculates the byte size of a string.
3939
* For non-ASCII, the byte size can be 2–4x the character count. For ASCII, there is no difference.
40+
* e.g. "Hello" has a byte size of 5, while "你" has a byte size of 3 and "😊" has a byte size of 4.
4041
*/
4142
internal val String.byteSize: Int
4243
get() = this.toByteArray(Charsets.UTF_8).size
Lines changed: 0 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,7 @@
11
package io.ably.lib.objects.unit
22

3-
import io.ably.lib.objects.*
4-
import io.ably.lib.objects.ObjectMessage
5-
import io.ably.lib.objects.size
6-
import io.ably.lib.types.AblyException
7-
import io.mockk.every
8-
import io.mockk.mockk
93
import kotlinx.coroutines.test.runTest
104
import org.junit.Test
11-
import kotlin.test.assertEquals
12-
import kotlin.test.assertFailsWith
135
import kotlin.test.assertNotNull
146

157
class LiveObjectTest {
@@ -19,34 +11,4 @@ class LiveObjectTest {
1911
val objects = channel.objects
2012
assertNotNull(objects)
2113
}
22-
23-
@Test
24-
fun testObjectMessageSizeWithinLimit() = runTest {
25-
val mockAdapter = mockk<LiveObjectsAdapter>()
26-
every { mockAdapter.maxMessageSizeLimit() } returns 65536L // 64 kb
27-
assertEquals(65536L, mockAdapter.maxMessageSizeLimit())
28-
29-
// Create ObjectMessage with dummy data that results in size 60 kb which is within the limit
30-
val objectMessage = ObjectMessage(
31-
clientId = CharArray(60 * 1024) { ('a'..'z').random() }.concatToString()
32-
)
33-
assertEquals(60 * 1024L, objectMessage.size()) // 60 kb (61,440 characters)
34-
// Doesn't throw exception, so test doesn't fail
35-
mockAdapter.ensureMessageSizeWithinLimit(arrayOf(objectMessage))
36-
37-
// Create ObjectMessage with dummy data that results in size 5kb
38-
val objectMessage2 = ObjectMessage(
39-
clientId = CharArray(5 * 1024) { ('a'..'z').random() }.concatToString()
40-
)
41-
assertEquals(5 * 1024L, objectMessage2.size()) // 5 kb
42-
43-
val exception = assertFailsWith<AblyException> {
44-
// both messages together with size of 65kb exceed the limit
45-
mockAdapter.ensureMessageSizeWithinLimit(arrayOf(objectMessage, objectMessage2))
46-
}
47-
// Assert on error code and message
48-
assertEquals(40009, exception.errorInfo.code)
49-
val expectedMessage = "ObjectMessage size 66560 exceeds maximum allowed size of 65536 bytes"
50-
assertEquals(expectedMessage, exception.errorInfo.message)
51-
}
5214
}
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
package io.ably.lib.objects.unit
2+
3+
import io.ably.lib.objects.*
4+
import io.ably.lib.objects.ObjectData
5+
import io.ably.lib.objects.ObjectMapOp
6+
import io.ably.lib.objects.ObjectMessage
7+
import io.ably.lib.objects.ObjectOperation
8+
import io.ably.lib.objects.ObjectOperationAction
9+
import io.ably.lib.objects.ObjectValue
10+
import io.ably.lib.objects.ensureMessageSizeWithinLimit
11+
import io.ably.lib.objects.size
12+
import io.ably.lib.transport.Defaults
13+
import io.ably.lib.types.AblyException
14+
import io.mockk.every
15+
import io.mockk.mockk
16+
import kotlinx.coroutines.test.runTest
17+
import org.junit.Test
18+
import kotlin.test.assertEquals
19+
import kotlin.test.assertFailsWith
20+
21+
class ObjectMessageSizeTest {
22+
23+
@Test
24+
fun testObjectMessageSizeWithinLimit() = runTest {
25+
val mockAdapter = mockk<LiveObjectsAdapter>()
26+
every { mockAdapter.maxMessageSizeLimit() } returns Defaults.maxMessageSize // 64 kb
27+
assertEquals(65536L, mockAdapter.maxMessageSizeLimit())
28+
29+
// ObjectMessage with all size-contributing fields
30+
val objectMessage = ObjectMessage(
31+
id = "msg_12345", // Not counted in size calculation
32+
timestamp = 1699123456789L, // Not counted in size calculation
33+
clientId = "test-client", // Size: 11 bytes (UTF-8 byte length)
34+
connectionId = "conn_98765", // Not counted in size calculation
35+
extras = mapOf( // Size: JSON serialization byte length
36+
"meta" to "data", // JSON: {"meta":"data","count":42}
37+
"count" to 42
38+
), // Total extras size: 26 bytes (verified by gson.toJson().length)
39+
operation = ObjectOperation(
40+
action = ObjectOperationAction.MapCreate,
41+
objectId = "obj_54321", // Not counted in operation size
42+
43+
// MapOp contributes to operation size
44+
mapOp = ObjectMapOp(
45+
key = "mapKey", // Size: 6 bytes (UTF-8 byte length)
46+
data = ObjectData(
47+
objectId = "ref_obj", // Not counted in data size
48+
encoding = "utf-8", // Not counted in data size
49+
value = ObjectValue("sample") // Size: 6 bytes (UTF-8 byte length)
50+
) // Total ObjectData size: 6 bytes
51+
), // Total ObjectMapOp size: 6 + 6 = 12 bytes
52+
53+
// CounterOp contributes to operation size
54+
counterOp = ObjectCounterOp(
55+
amount = 10.5 // Size: 8 bytes (number is always 8 bytes)
56+
), // Total ObjectCounterOp size: 8 bytes
57+
58+
// Map contributes to operation size (for MAP_CREATE operations)
59+
map = ObjectMap(
60+
semantics = MapSemantics.LWW, // Not counted in size
61+
entries = mapOf(
62+
"entry1" to ObjectMapEntry( // Key size: 6 bytes
63+
tombstone = false, // Not counted in entry size
64+
timeserial = "ts_123", // Not counted in entry size
65+
data = ObjectData(
66+
value = ObjectValue("value1") // Size: 6 bytes
67+
) // ObjectMapEntry size: 6 bytes
68+
), // Total for this entry: 6 (key) + 6 (entry) = 12 bytes
69+
"entry2" to ObjectMapEntry( // Key size: 6 bytes
70+
data = ObjectData(
71+
value = ObjectValue(42) // Size: 8 bytes (number)
72+
) // ObjectMapEntry size: 8 bytes
73+
) // Total for this entry: 6 (key) + 8 (entry) = 14 bytes
74+
) // Total entries size: 12 + 14 = 26 bytes
75+
), // Total ObjectMap size: 26 bytes
76+
77+
// Counter contributes to operation size (for COUNTER_CREATE operations)
78+
counter = ObjectCounter(
79+
count = 100.0 // Size: 8 bytes (number is always 8 bytes)
80+
), // Total ObjectCounter size: 8 bytes
81+
82+
nonce = "nonce123", // Not counted in operation size
83+
initialValue = Binary("initial".toByteArray()), // Not counted in operation size
84+
initialValueEncoding = ProtocolMessageFormat.Json // Not counted in operation size
85+
), // Total ObjectOperation size: 12 + 8 + 26 + 8 = 54 bytes
86+
87+
objectState = ObjectState(
88+
objectId = "state_obj", // Not counted in state size
89+
siteTimeserials = mapOf("site1" to "serial1"), // Not counted in state size
90+
tombstone = false, // Not counted in state size
91+
92+
// createOp contributes to state size
93+
createOp = ObjectOperation(
94+
action = ObjectOperationAction.MapSet,
95+
objectId = "create_obj",
96+
mapOp = ObjectMapOp(
97+
key = "createKey", // Size: 9 bytes
98+
data = ObjectData(
99+
value = ObjectValue("createValue") // Size: 11 bytes
100+
) // ObjectData size: 11 bytes
101+
) // ObjectMapOp size: 9 + 11 = 20 bytes
102+
), // Total createOp size: 20 bytes
103+
104+
// map contributes to state size
105+
map = ObjectMap(
106+
entries = mapOf(
107+
"stateKey" to ObjectMapEntry( // Key size: 8 bytes
108+
data = ObjectData(
109+
value = ObjectValue("stateValue") // Size: 10 bytes
110+
) // ObjectMapEntry size: 10 bytes
111+
) // Total: 8 + 10 = 18 bytes
112+
)
113+
), // Total ObjectMap size: 18 bytes
114+
115+
// counter contributes to state size
116+
counter = ObjectCounter(
117+
count = 50.0 // Size: 8 bytes
118+
) // Total ObjectCounter size: 8 bytes
119+
), // Total ObjectState size: 20 + 18 + 8 = 46 bytes
120+
121+
serial = "serial_123", // Not counted in size calculation
122+
siteCode = "site_abc" // Not counted in size calculation
123+
)
124+
125+
// clientId: 11 bytes + operation: 54 bytes + objectState: 46 bytes + extras: 26 bytes = 137 bytes
126+
val messageSize = objectMessage.size()
127+
assertEquals(137, messageSize)
128+
129+
// Verify the message doesn't exceed the maxMessageSize limit
130+
mockAdapter.ensureMessageSizeWithinLimit(arrayOf(objectMessage))
131+
}
132+
133+
@Test
134+
fun testObjectMessageSizeForUnicodeCharacters() = runTest {
135+
val objectMessage = ObjectMessage(
136+
operation = ObjectOperation(
137+
objectId = "",
138+
action = ObjectOperationAction.MapCreate,
139+
mapOp = ObjectMapOp(
140+
key = "",
141+
data = ObjectData(
142+
value = ObjectValue("你😊") // 你 -> 3 bytes, 😊 -> 4 bytes
143+
),
144+
),
145+
)
146+
)
147+
assertEquals(7, objectMessage.size())
148+
}
149+
150+
@Test
151+
fun testObjectMessageSizeAboveLimit() = runTest {
152+
val mockAdapter = mockk<LiveObjectsAdapter>()
153+
every { mockAdapter.maxMessageSizeLimit() } returns Defaults.maxMessageSize // 64 kb
154+
155+
// Create ObjectMessage with dummy data that results in size 60kb
156+
val objectMessage1 = ObjectMessage(
157+
clientId = CharArray(60 * 1024) { ('a'..'z').random() }.concatToString()
158+
)
159+
assertEquals(60 * 1024L, objectMessage1.size())
160+
161+
// Create ObjectMessage with dummy data that results in size 5kb
162+
val objectMessage2 = ObjectMessage(
163+
clientId = CharArray(5 * 1024) { ('a'..'z').random() }.concatToString()
164+
)
165+
assertEquals(5 * 1024L, objectMessage2.size())
166+
167+
val exception = assertFailsWith<AblyException> {
168+
mockAdapter.ensureMessageSizeWithinLimit(arrayOf(objectMessage1, objectMessage2)) // sum size = 65kb exceeds limit
169+
}
170+
// Assert on error code and message
171+
assertEquals(40009, exception.errorInfo.code)
172+
val expectedMessage = "ObjectMessage size 66560 exceeds maximum allowed size of 65536 bytes"
173+
assertEquals(expectedMessage, exception.errorInfo.message)
174+
}
175+
}

0 commit comments

Comments
 (0)