Andrey V
01/13/2025, 9:00 AMcallBy
In which cases, for @Serializable data class C
and applicable primary constructor params ...
, can the following expression
((::C).call(...)::copy).callBy(emptyMap())
fail with
KotlinReflectionInternalError(
"Inconsistent number of parameters in the descriptor and Java reflection object: $arity != $expectedArgsSize\n" +
"Calling: $descriptor\n" +
"Parameter types: ${this.parameterTypes})\n" +
"Default: $isDefault
with expectedArgsSize
being the number of primary constructor declared properties + 1 and arity being the number of primary constructor declared properties + 2?
It seems the -1 is due to the reflection logic determining that it's a bound reference to a static method, which seems nonsensical
Reducing the case to ~100 lines made the issue vanish so I'm looking for things to pay attention to for debuggingAndrey V
01/13/2025, 9:27 PMinternal class CheckArg<R> private constructor (
val argsMap: Map<String, Any?>,
private val expectedException: KClass<out Throwable>?,
val matcher: (Any?) -> Unit, // (String)->Unit | (R) -> Unit
) {
private fun resolveArgs(params: List<KParameter>): Map<KParameter, Any?> =
params.mapNotNull {
when {
it.kind == KParameter.Kind.VALUE && it.name in argsMap ->
it to argsMap[it.name]
else ->
null
}
}.toMap()
operator fun invoke(func: KCallable<R>, post: (R) -> Unit = {}) {
val args = resolveArgs(func.parameters)
if (expectedException != null) {
val t = assertThrows(expectedException.java) {
try {
func.callBy(args)
} catch (e: InvocationTargetException) {
throw e.targetException
}
}
matcher(t.message)
} else {
val a = assertDoesNotThrow { func.callBy(args) }
matcher(a)
post(a)
}
}
companion object {
/* reifying quasi-constructors */
/** [CheckArg] expecting an exception with Regex message matcher. */
inline operator fun <R, reified Th: Throwable> invoke(
args: Map<String, Any?>,
matchStr: String
) =
CheckArg<R>(args, Th::class) { assertTrue((it as? String)?.contains(matchStr) ?: false) }
/** [CheckArg] not expecting an exception with result value matcher. */
@Suppress("UNCHECKED_CAST")
operator fun <R> invoke(
args: Map<String, Any?>,
matcher: (R) -> Unit = {}
) =
CheckArg<R>(args, null) { matcher(it as R) }
}
}
so that we can use an ArgumentsProvider producing a Stream of CheckArg<R, ExceptionType>(argsMap, exceptionMsg)
and the test function is
@ParameterizedTest
@ArgumentsSource(ArgSource::class)
fun test(a: CheckArg<R>) {
if (a.argsMap.size == 2) {
val ctor: (...) -> C = ::C
a(ctor as KCallable<C>) { instance = it }
} else
a(c!!::copy)
}
thus allowing the approach of
"inputs A=A, B=B produce a value C which can then be asserted for whatever (or saved as an object to copy from)",
"input D=D produces exception E containing message F"
through relatively straightforward parameterization
If the test function is simplified to just invoking the ctor and doing the map update ourselves instead of passing a changed-args map to (instance::copy).callBy
, the issue disappears