Skip to content

Commit da020f9

Browse files
authored
Improve test for JVM intrinsics (#2642)
Old one used time-based approach. It was a good indicator, but in rare circumstances it may have been flaky and produced incorrect results. Flaky time-based tests are problematic for large builds, such as Kotlin's Aggregate build. New approach is based on cache presence and should not give incorrect results. See also #KTI-1726
1 parent c7bcaf1 commit da020f9

File tree

6 files changed

+40
-20
lines changed

6 files changed

+40
-20
lines changed

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,4 +252,4 @@ tasks.named("dokkaHtmlMultiModule") {
252252

253253
tasks.withType(org.jetbrains.kotlin.gradle.targets.js.npm.tasks.KotlinNpmInstallTask).configureEach {
254254
args.add("--ignore-engines")
255-
}
255+
}

core/commonMain/src/kotlinx/serialization/SerializersCache.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import kotlin.reflect.KType
1919
* Cache for non-null non-parametrized and non-contextual serializers.
2020
*/
2121
@ThreadLocal
22-
private val SERIALIZERS_CACHE = createCache { it.serializerOrNull() ?: it.polymorphicIfInterface() }
22+
internal val SERIALIZERS_CACHE = createCache { it.serializerOrNull() ?: it.polymorphicIfInterface() }
2323

2424
/**
2525
* Cache for nullable non-parametrized and non-contextual serializers.

core/commonMain/src/kotlinx/serialization/internal/Platform.common.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ package kotlinx.serialization.internal
66

77
import kotlinx.serialization.*
88
import kotlinx.serialization.descriptors.*
9-
import kotlin.native.concurrent.*
109
import kotlin.reflect.*
1110

1211
internal object InternalHexConverter {
@@ -169,6 +168,13 @@ internal interface SerializerCache<T> {
169168
* Returns cached serializer or `null` if serializer not found.
170169
*/
171170
fun get(key: KClass<Any>): KSerializer<T>?
171+
172+
/**
173+
* Use SOLELY for test purposes.
174+
* May return `false` even if `get` returns value. It means that entry was computed, but not
175+
* stored (behavior for all non-JVM platforms).
176+
*/
177+
fun isStored(key: KClass<*>): Boolean = false
172178
}
173179

174180
/**

core/commonTest/src/kotlinx/serialization/CachedSerializersTest.kt

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -61,21 +61,5 @@ class CachedSerializersTest {
6161
fun testAbstractSerializersAreSame() {
6262
assertSame(Abstract.serializer(), Abstract.serializer())
6363
}
64-
65-
66-
@OptIn(ExperimentalTime::class)
67-
@Test
68-
fun testSerializersAreIntrinsified() = jvmOnly {
69-
val m = SerializersModule { }
70-
val direct = measureTime {
71-
Object.serializer()
72-
}
73-
val directMs = direct.inWholeMicroseconds
74-
val indirect = measureTime {
75-
m.serializer<Object>()
76-
}
77-
val indirectMs = indirect.inWholeMicroseconds
78-
if (indirectMs > directMs + (directMs / 4)) error("Direct ($directMs) and indirect ($indirectMs) times are too far apart")
79-
}
8064
}
8165

core/jvmMain/src/kotlinx/serialization/internal/Caching.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ private class ClassValueCache<T>(val compute: (KClass<*>) -> KSerializer<T>?) :
5252
.getOrSet(key.java) { CacheEntry(compute(key)) }
5353
.serializer
5454
}
55+
56+
override fun isStored(key: KClass<*>): Boolean {
57+
return classValue.isStored(key.java)
58+
}
5559
}
5660

5761
/**
@@ -85,6 +89,11 @@ private class ClassValueReferences<T> : ClassValue<MutableSoftReference<T>>() {
8589
return ref.getOrSetWithLock { factory() }
8690
}
8791

92+
fun isStored(key: Class<*>): Boolean {
93+
val ref = get(key)
94+
return ref.reference.get() != null
95+
}
96+
8897
}
8998

9099
/**
@@ -134,6 +143,10 @@ private class ConcurrentHashMapCache<T>(private val compute: (KClass<*>) -> KSer
134143
CacheEntry(compute(key))
135144
}.serializer
136145
}
146+
147+
override fun isStored(key: KClass<*>): Boolean {
148+
return cache.containsKey(key.java)
149+
}
137150
}
138151

139152

core/jvmTest/src/kotlinx/serialization/CachingTest.kt

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ package kotlinx.serialization
66

77
import kotlinx.serialization.internal.*
88
import kotlinx.serialization.modules.*
9-
import org.junit.Test
109
import kotlin.reflect.*
1110
import kotlin.test.*
11+
import kotlin.test.Test
1212

1313
class CachingTest {
1414
@Test
@@ -43,4 +43,21 @@ class CachingTest {
4343

4444
assertEquals(1, factoryCalled)
4545
}
46+
47+
@Serializable
48+
class Target
49+
50+
@Test
51+
fun testJvmIntrinsics() {
52+
val ser1 = Target.serializer()
53+
assertFalse(SERIALIZERS_CACHE.isStored(Target::class), "Cache shouldn't have values before call to serializer<T>()")
54+
val ser2 = serializer<Target>()
55+
assertFalse(
56+
SERIALIZERS_CACHE.isStored(Target::class),
57+
"Serializer for Target::class is stored in the cache, which means that runtime lookup was performed and call to serializer<Target> was not intrinsified." +
58+
"Check that compiler plugin intrinsics are enabled and working correctly."
59+
)
60+
val ser3 = serializer(typeOf<Target>())
61+
assertTrue(SERIALIZERS_CACHE.isStored(Target::class), "Serializer should be stored in cache after typeOf-based lookup")
62+
}
4663
}

0 commit comments

Comments
 (0)