Skip to content

Commit 8005161

Browse files
Jozott00Mr3zee
authored andcommitted
grpc-common: Add field presence tracking, required field enforcing and generation of the MessageCodec implementation (#421)
* grpc-pb: Refactor subtyping hierarchy Signed-off-by: Johannes Zottele <[email protected]> * grpc-pb: Add presence tracking and required field check Signed-off-by: Johannes Zottele <[email protected]> * grpc-pb: Add PresenceIndices object that holds the presence indices of all fields. Signed-off-by: Johannes Zottele <[email protected]> * grpc-pb: Move BitSet to utils Signed-off-by: Johannes Zottele <[email protected]> * grpc-pb: Add MessageCodec object for each message Signed-off-by: Johannes Zottele <[email protected]> * grpc-pb: Use only fully qualified names for kotlinx.rpc.grpc.pb.* classes Signed-off-by: Johannes Zottele <[email protected]> * Revert kotlin version increase Signed-off-by: Johannes Zottele <[email protected]> * grpc-pb: Remove demo test Signed-off-by: Johannes Zottele <[email protected]> * grpc-pb: Address PR comments Signed-off-by: Johannes Zottele <[email protected]> --------- Signed-off-by: Johannes Zottele <[email protected]>
1 parent d7f0bc0 commit 8005161

File tree

12 files changed

+603
-67
lines changed

12 files changed

+603
-67
lines changed

gradle-conventions/src/main/kotlin/conventions-kotlin-version.gradle.kts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import org.jetbrains.kotlin.gradle.dsl.KotlinCommonCompilerOptions
66
import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension
77
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
8-
import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation
98
import util.withKotlinJvmExtension
109
import util.withKotlinKmpExtension
1110

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/*
2+
* Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.rpc.grpc.pb
6+
7+
import kotlinx.rpc.grpc.utils.BitSet
8+
import kotlinx.rpc.internal.utils.InternalRpcApi
9+
10+
@InternalRpcApi
11+
public abstract class InternalMessage(fieldsWithPresence: Int) {
12+
public val presenceMask: BitSet = BitSet(fieldsWithPresence)
13+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.rpc.grpc.utils
6+
7+
import kotlinx.rpc.internal.utils.InternalRpcApi
8+
9+
/**
10+
* A fixed-sized vector of bits, allowing one to set/clear/read bits from it by a bit index.
11+
*/
12+
@InternalRpcApi
13+
public class BitSet(public val size: Int) {
14+
private val data: LongArray = LongArray((size + 63) ushr 6)
15+
16+
/** Sets the bit at [index] to 1. */
17+
public operator fun set(index: Int, value: Boolean) {
18+
if (!value) return clear(index)
19+
require(index in 0 until size) { "Index $index out‑of‑bounds for length $size" }
20+
val word = index ushr 6
21+
val mask = 1L shl (index and 63)
22+
data[word] = data[word] or mask
23+
}
24+
25+
/** Clears the bit at [index] (sets to 0). */
26+
public fun clear(index: Int) {
27+
require(index >= 0 && index < size) { "Index $index out of bounds for length $size" }
28+
val word = index ushr 6
29+
data[word] = data[word] and (1L shl (index and 63)).inv()
30+
}
31+
32+
/** Returns true if the bit at [index] is set. */
33+
public operator fun get(index: Int): Boolean {
34+
require(index >= 0 && index < size) { "Index $index out of bounds for length $size" }
35+
val word = index ushr 6
36+
return (data[word] ushr (index and 63) and 1L) != 0L
37+
}
38+
39+
/** Clears all bits. */
40+
public fun clearAll() {
41+
data.fill(0L)
42+
}
43+
44+
/** Returns the number of bits set to 1. */
45+
public fun cardinality(): Int {
46+
var sum = 0
47+
for (w in data) {
48+
sum += w.countOneBits()
49+
}
50+
return sum
51+
}
52+
53+
/** Returns true if all bits are set. */
54+
public fun allSet(): Boolean {
55+
val fullWords = size ushr 6
56+
// check full 64-bit words
57+
for (i in 0 until fullWords) {
58+
if (data[i] != -1L) return false
59+
}
60+
// check leftover bits
61+
val rem = size and 63
62+
if (rem != 0) {
63+
val mask = (-1L ushr (64 - rem))
64+
if (data[fullWords] != mask) return false
65+
}
66+
return true
67+
}
68+
}
Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
1+
/*
2+
* Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.rpc.grpc.internal
6+
7+
import kotlinx.rpc.grpc.utils.BitSet
8+
import kotlin.test.*
9+
10+
class BitSetTest {
11+
12+
@Test
13+
fun testConstructor() {
14+
// Test with size 0
15+
val bitSet0 = BitSet(0)
16+
assertEquals(0, bitSet0.size)
17+
assertEquals(0, bitSet0.cardinality())
18+
19+
// Test with small size
20+
val bitSet10 = BitSet(10)
21+
assertEquals(10, bitSet10.size)
22+
assertEquals(0, bitSet10.cardinality())
23+
24+
// Test with size that spans multiple words
25+
val bitSet100 = BitSet(100)
26+
assertEquals(100, bitSet100.size)
27+
assertEquals(0, bitSet100.cardinality())
28+
29+
// Test with size at word boundary
30+
val bitSet64 = BitSet(64)
31+
assertEquals(64, bitSet64.size)
32+
assertEquals(0, bitSet64.cardinality())
33+
34+
// Test with size just over word boundary
35+
val bitSet65 = BitSet(65)
36+
assertEquals(65, bitSet65.size)
37+
assertEquals(0, bitSet65.cardinality())
38+
}
39+
40+
@Test
41+
fun testSetAndGet() {
42+
val bitSet = BitSet(100)
43+
44+
// Initially all bits should be unset
45+
for (i in 0 until 100) {
46+
assertFalse(bitSet[i], "Bit $i should be initially unset")
47+
}
48+
49+
// Set some bits
50+
bitSet[0] = true
51+
bitSet[1] = true
52+
bitSet[63] = true
53+
bitSet[64] = true
54+
bitSet[99] = true
55+
56+
// Verify the bits are set
57+
assertTrue(bitSet[0], "Bit 0 should be set")
58+
assertTrue(bitSet[1], "Bit 1 should be set")
59+
assertTrue(bitSet[63], "Bit 63 should be set")
60+
assertTrue(bitSet[64], "Bit 64 should be set")
61+
assertTrue(bitSet[99], "Bit 99 should be set")
62+
63+
// Verify other bits are still unset
64+
assertFalse(bitSet[2], "Bit 2 should be unset")
65+
assertFalse(bitSet[62], "Bit 62 should be unset")
66+
assertFalse(bitSet[65], "Bit 65 should be unset")
67+
assertFalse(bitSet[98], "Bit 98 should be unset")
68+
}
69+
70+
@Test
71+
fun testClear() {
72+
val bitSet = BitSet(100)
73+
74+
// Set all bits
75+
for (i in 0 until 100) {
76+
bitSet[i] = true
77+
}
78+
79+
// Verify all bits are set
80+
for (i in 0 until 100) {
81+
assertTrue(bitSet[i], "Bit $i should be set")
82+
}
83+
84+
// Clear some bits
85+
bitSet[0] = false
86+
bitSet[1] = false
87+
bitSet[63] = false
88+
bitSet[64] = false
89+
bitSet[99] = false
90+
91+
// Verify the bits are cleared
92+
assertFalse(bitSet[0], "Bit 0 should be cleared")
93+
assertFalse(bitSet[1], "Bit 1 should be cleared")
94+
assertFalse(bitSet[63], "Bit 63 should be cleared")
95+
assertFalse(bitSet[64], "Bit 64 should be cleared")
96+
assertFalse(bitSet[99], "Bit 99 should be cleared")
97+
98+
// Verify other bits are still set
99+
assertTrue(bitSet[2], "Bit 2 should still be set")
100+
assertTrue(bitSet[62], "Bit 62 should still be set")
101+
assertTrue(bitSet[65], "Bit 65 should still be set")
102+
assertTrue(bitSet[98], "Bit 98 should still be set")
103+
}
104+
105+
@Test
106+
fun testClearAll() {
107+
val bitSet = BitSet(100)
108+
109+
// Set all bits
110+
for (i in 0 until 100) {
111+
bitSet[i] = true
112+
}
113+
114+
// Verify all bits are set
115+
for (i in 0 until 100) {
116+
assertTrue(bitSet[i], "Bit $i should be set")
117+
}
118+
119+
// Clear all bits
120+
bitSet.clearAll()
121+
122+
// Verify all bits are cleared
123+
for (i in 0 until 100) {
124+
assertFalse(bitSet[i], "Bit $i should be cleared after clearAll")
125+
}
126+
}
127+
128+
@Test
129+
fun testCardinality() {
130+
val bitSet = BitSet(100)
131+
assertEquals(0, bitSet.cardinality(), "Initial cardinality should be 0")
132+
133+
// Set some bits
134+
bitSet[0] = true
135+
assertEquals(1, bitSet.cardinality(), "Cardinality should be 1 after setting 1 bit")
136+
137+
bitSet[63] = true
138+
assertEquals(2, bitSet.cardinality(), "Cardinality should be 2 after setting 2 bits")
139+
140+
bitSet[64] = true
141+
assertEquals(3, bitSet.cardinality(), "Cardinality should be 3 after setting 3 bits")
142+
143+
bitSet[99] = true
144+
assertEquals(4, bitSet.cardinality(), "Cardinality should be 4 after setting 4 bits")
145+
146+
// Clear a bit
147+
bitSet.clear(0)
148+
assertEquals(3, bitSet.cardinality(), "Cardinality should be 3 after clearing 1 bit")
149+
150+
// Set a bit that's already set
151+
bitSet[63] = true
152+
assertEquals(3, bitSet.cardinality(), "Cardinality should still be 3 after setting an already set bit")
153+
154+
// Clear all bits
155+
bitSet.clearAll()
156+
assertEquals(0, bitSet.cardinality(), "Cardinality should be 0 after clearAll")
157+
}
158+
159+
@Test
160+
fun testAllSet() {
161+
// Test with empty BitSet
162+
val emptyBitSet = BitSet(0)
163+
assertTrue(emptyBitSet.allSet(), "Empty BitSet should return true for allSet")
164+
165+
// Test with small BitSet
166+
val smallBitSet = BitSet(5)
167+
assertFalse(smallBitSet.allSet(), "New BitSet should return false for allSet")
168+
169+
smallBitSet[0] = true
170+
smallBitSet[1] = true
171+
smallBitSet[2] = true
172+
smallBitSet[3] = true
173+
smallBitSet[4] = true
174+
assertTrue(smallBitSet.allSet(), "BitSet with all bits set should return true for allSet")
175+
176+
smallBitSet.clear(2)
177+
assertFalse(smallBitSet.allSet(), "BitSet with one bit cleared should return false for allSet")
178+
179+
// Test with BitSet that spans multiple words
180+
val largeBitSet = BitSet(100)
181+
assertFalse(largeBitSet.allSet(), "New large BitSet should return false for allSet")
182+
183+
for (i in 0 until 100) {
184+
largeBitSet[i] = true
185+
}
186+
assertTrue(largeBitSet.allSet(), "Large BitSet with all bits set should return true for allSet")
187+
188+
largeBitSet.clear(63)
189+
assertFalse(largeBitSet.allSet(), "Large BitSet with one bit cleared should return false for allSet")
190+
191+
// Test with BitSet at word boundary
192+
val wordBoundaryBitSet = BitSet(64)
193+
assertFalse(wordBoundaryBitSet.allSet(), "New word boundary BitSet should return false for allSet")
194+
195+
for (i in 0 until 64) {
196+
wordBoundaryBitSet[i] = true
197+
}
198+
assertTrue(wordBoundaryBitSet.allSet(), "Word boundary BitSet with all bits set should return true for allSet")
199+
}
200+
201+
@Test
202+
fun testEdgeCases() {
203+
val bitSet = BitSet(100)
204+
205+
// Test setting and getting at boundaries
206+
bitSet[0] = true
207+
assertTrue(bitSet[0], "Should be able to set and get bit 0")
208+
209+
bitSet[99] = true
210+
assertTrue(bitSet[99], "Should be able to set and get bit at size-1")
211+
212+
// Test clearing at boundaries
213+
bitSet.clear(0)
214+
assertFalse(bitSet[0], "Should be able to clear bit 0")
215+
216+
bitSet.clear(99)
217+
assertFalse(bitSet[99], "Should be able to clear bit at size-1")
218+
219+
// Test out of bounds access
220+
assertFailsWith<IllegalArgumentException> {
221+
bitSet[100] = true
222+
}
223+
224+
assertFailsWith<IllegalArgumentException> {
225+
bitSet.clear(100)
226+
}
227+
228+
assertFailsWith<IllegalArgumentException> {
229+
bitSet[100]
230+
}
231+
232+
assertFailsWith<IllegalArgumentException> {
233+
bitSet[-1] = true
234+
}
235+
236+
assertFailsWith<IllegalArgumentException> {
237+
bitSet.clear(-1)
238+
}
239+
240+
assertFailsWith<IllegalArgumentException> {
241+
bitSet[-1]
242+
}
243+
}
244+
245+
@Test
246+
fun testWordBoundaries() {
247+
// Test BitSet with size at word boundaries
248+
for (size in listOf(63, 64, 65, 127, 128, 129)) {
249+
val bitSet = BitSet(size)
250+
251+
// Set all bits
252+
for (i in 0 until size) {
253+
bitSet[i] = true
254+
}
255+
256+
// Verify all bits are set
257+
for (i in 0 until size) {
258+
assertTrue(bitSet[i], "Bit $i should be set in BitSet of size $size")
259+
}
260+
261+
// Verify cardinality
262+
assertEquals(size, bitSet.cardinality(), "Cardinality should equal size for fully set BitSet")
263+
264+
// Verify allSet
265+
assertTrue(bitSet.allSet(), "allSet should return true for fully set BitSet")
266+
267+
// Clear all bits
268+
bitSet.clearAll()
269+
270+
// Verify all bits are cleared
271+
for (i in 0 until size) {
272+
assertFalse(bitSet[i], "Bit $i should be cleared in BitSet of size $size after clearAll")
273+
}
274+
275+
// Verify cardinality
276+
assertEquals(0, bitSet.cardinality(), "Cardinality should be 0 after clearAll")
277+
278+
// Verify allSet
279+
assertFalse(bitSet.allSet(), "allSet should return false after clearAll")
280+
}
281+
}
282+
283+
@Test
284+
fun testLargeCardinality() {
285+
// Test with a large BitSet to verify cardinality calculation
286+
val size = 1000
287+
val bitSet = BitSet(size)
288+
289+
// Set every other bit
290+
for (i in 0 until size step 2) {
291+
bitSet[i] = true
292+
}
293+
294+
// Verify cardinality
295+
assertEquals(size / 2, bitSet.cardinality(), "Cardinality should be half the size when every other bit is set")
296+
297+
// Set all bits
298+
for (i in 0 until size) {
299+
bitSet[i] = true
300+
}
301+
302+
// Verify cardinality
303+
assertEquals(size, bitSet.cardinality(), "Cardinality should equal size when all bits are set")
304+
}
305+
}

0 commit comments

Comments
 (0)