diff --git a/documentation/modules/ROOT/partials/release-notes/release-notes-6.1.0-M2.adoc b/documentation/modules/ROOT/partials/release-notes/release-notes-6.1.0-M2.adoc index ba9b0fb0fa68..547f55b01bb9 100644 --- a/documentation/modules/ROOT/partials/release-notes/release-notes-6.1.0-M2.adoc +++ b/documentation/modules/ROOT/partials/release-notes/release-notes-6.1.0-M2.adoc @@ -52,6 +52,10 @@ repository on GitHub. * Introduce new `trimStacktrace(Class)` and `retainStackTraceElements(int)` methods for `AssertionFailureBuilder`. These allow user defined assertions to trim their stacktrace. +* Generic inline value classes (such as `kotlin.Result`) can now be used as parameters +in `@ParameterizedTest` methods when `kotlin-reflect` is on the classpath. +Note: Primitive-wrapper inline value classes (e.g., `UInt`, custom value classes wrapping +primitives) are not yet supported. See https://github.com/junit-team/junit-framework/issues/5081[#5081]. [[v6.1.0-M2-junit-vintage]] diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/support/MethodReflectionUtils.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/support/MethodReflectionUtils.java index 02c8ca228ff6..8aa593899cac 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/support/MethodReflectionUtils.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/support/MethodReflectionUtils.java @@ -13,15 +13,19 @@ import static org.apiguardian.api.API.Status.INTERNAL; import static org.junit.platform.commons.util.KotlinReflectionUtils.getKotlinSuspendingFunctionGenericReturnType; import static org.junit.platform.commons.util.KotlinReflectionUtils.getKotlinSuspendingFunctionReturnType; +import static org.junit.platform.commons.util.KotlinReflectionUtils.invokeKotlinFunction; import static org.junit.platform.commons.util.KotlinReflectionUtils.invokeKotlinSuspendingFunction; import static org.junit.platform.commons.util.KotlinReflectionUtils.isKotlinSuspendingFunction; +import static org.junit.platform.commons.util.KotlinReflectionUtils.isKotlinType; import java.lang.reflect.Method; import java.lang.reflect.Type; +import java.util.Arrays; import org.apiguardian.api.API; import org.jspecify.annotations.Nullable; import org.junit.platform.commons.support.ReflectionSupport; +import org.junit.platform.commons.util.KotlinReflectionUtils; @API(status = INTERNAL, since = "6.0") public class MethodReflectionUtils { @@ -42,9 +46,21 @@ public static Type getGenericReturnType(Method method) { if (isKotlinSuspendingFunction(method)) { return invokeKotlinSuspendingFunction(method, target, arguments); } + if (isKotlinType(method.getDeclaringClass()) && KotlinReflectionUtils.isKotlinReflectPresent() + && hasInlineTypeArgument(arguments)) { + return invokeKotlinFunction(method, target, arguments); + } return ReflectionSupport.invokeMethod(method, target, arguments); } + private static boolean hasInlineTypeArgument(@Nullable Object[] arguments) { + if (!KotlinReflectionUtils.isKotlinReflectPresent()) { + return false; + } + + return arguments.length > 0 && Arrays.stream(arguments).anyMatch(KotlinReflectionUtils::isInstanceOfInlineType); + } + private MethodReflectionUtils() { } } diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/util/KotlinSuspendingFunctionUtils.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/KotlinFunctionUtils.java similarity index 77% rename from junit-platform-commons/src/main/java/org/junit/platform/commons/util/KotlinSuspendingFunctionUtils.java rename to junit-platform-commons/src/main/java/org/junit/platform/commons/util/KotlinFunctionUtils.java index fe2f348fea31..9d75be425d92 100644 --- a/junit-platform-commons/src/main/java/org/junit/platform/commons/util/KotlinSuspendingFunctionUtils.java +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/KotlinFunctionUtils.java @@ -37,7 +37,7 @@ import kotlin.reflect.KParameter; import kotlin.reflect.jvm.ReflectJvmMapping; -class KotlinSuspendingFunctionUtils { +class KotlinFunctionUtils { static Class getReturnType(Method method) { var returnType = getJavaClass(getJvmErasure(getKotlinFunction(method).getReturnType())); @@ -67,17 +67,35 @@ static Class[] getParameterTypes(Method method) { return Arrays.stream(method.getParameterTypes()).limit(parameterCount - 1).toArray(Class[]::new); } - static @Nullable Object invoke(Method method, @Nullable Object target, @Nullable Object[] args) { + static @Nullable Object invokeKotlinFunction(Method method, @Nullable Object target, @Nullable Object[] args) { try { - return invoke(getKotlinFunction(method), target, args); + return invokeKotlinFunction(getKotlinFunction(method), target, args); } catch (InterruptedException e) { throw throwAsUncheckedException(e); } } - private static @Nullable T invoke(KFunction function, @Nullable Object target, @Nullable Object[] args) - throws InterruptedException { + private static T invokeKotlinFunction(KFunction function, @Nullable Object target, + @Nullable Object[] args) throws InterruptedException { + if (!isAccessible(function)) { + setAccessible(function, true); + } + return function.callBy(toArgumentMap(target, args, function)); + } + + static @Nullable Object invokeKotlinSuspendingFunction(Method method, @Nullable Object target, + @Nullable Object[] args) { + try { + return invokeKotlinSuspendingFunction(getKotlinFunction(method), target, args); + } + catch (InterruptedException e) { + throw throwAsUncheckedException(e); + } + } + + private static T invokeKotlinSuspendingFunction(KFunction function, + @Nullable Object target, @Nullable Object[] args) throws InterruptedException { if (!isAccessible(function)) { setAccessible(function, true); } @@ -113,6 +131,6 @@ private static KFunction getKotlinFunction(Method method) { () -> "Failed to get Kotlin function for method: " + method); } - private KotlinSuspendingFunctionUtils() { + private KotlinFunctionUtils() { } } diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/util/KotlinReflectionUtils.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/KotlinReflectionUtils.java index f3a04fe2e1a7..e804bf74c017 100644 --- a/junit-platform-commons/src/main/java/org/junit/platform/commons/util/KotlinReflectionUtils.java +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/KotlinReflectionUtils.java @@ -37,6 +37,7 @@ public class KotlinReflectionUtils { private static final String DEFAULT_IMPLS_CLASS_NAME = "DefaultImpls"; private static final @Nullable Class kotlinMetadata; + private static final @Nullable Class jvmInline; private static final @Nullable Class kotlinCoroutineContinuation; private static final boolean kotlinReflectPresent; private static final boolean kotlinxCoroutinesPresent; @@ -44,6 +45,7 @@ public class KotlinReflectionUtils { static { var metadata = tryToLoadKotlinMetadataClass(); kotlinMetadata = metadata.toOptional().orElse(null); + jvmInline = tryToLoadJvmInlineClass().toOptional().orElse(null); kotlinCoroutineContinuation = metadata // .andThen(__ -> tryToLoadClass("kotlin.coroutines.Continuation")) // .toOptional() // @@ -62,6 +64,12 @@ private static Try> tryToLoadKotlinMetadataClass() { .andThenTry(it -> (Class) it); } + @SuppressWarnings("unchecked") + private static Try> tryToLoadJvmInlineClass() { + return tryToLoadClass("kotlin.jvm.JvmInline") // + .andThenTry(it -> (Class) it); + } + /** * @since 6.0 */ @@ -117,36 +125,54 @@ private static Class[] copyWithoutFirst(Class[] values) { return result; } - private static boolean isKotlinType(Class clazz) { + @API(status = INTERNAL, since = "6.1") + public static boolean isKotlinType(Class clazz) { return kotlinMetadata != null // && clazz.getDeclaredAnnotation(kotlinMetadata) != null; } + @API(status = INTERNAL, since = "6.1") + public static boolean isKotlinReflectPresent() { + return kotlinReflectPresent; + } + public static Class getKotlinSuspendingFunctionReturnType(Method method) { requireKotlinReflect(method); - return KotlinSuspendingFunctionUtils.getReturnType(method); + return KotlinFunctionUtils.getReturnType(method); } public static Type getKotlinSuspendingFunctionGenericReturnType(Method method) { requireKotlinReflect(method); - return KotlinSuspendingFunctionUtils.getGenericReturnType(method); + return KotlinFunctionUtils.getGenericReturnType(method); } public static Parameter[] getKotlinSuspendingFunctionParameters(Method method) { requireKotlinReflect(method); - return KotlinSuspendingFunctionUtils.getParameters(method); + return KotlinFunctionUtils.getParameters(method); } public static Class[] getKotlinSuspendingFunctionParameterTypes(Method method) { requireKotlinReflect(method); - return KotlinSuspendingFunctionUtils.getParameterTypes(method); + return KotlinFunctionUtils.getParameterTypes(method); } public static @Nullable Object invokeKotlinSuspendingFunction(Method method, @Nullable Object target, @Nullable Object[] args) { requireKotlinReflect(method); requireKotlinxCoroutines(method); - return KotlinSuspendingFunctionUtils.invoke(method, target, args); + return KotlinFunctionUtils.invokeKotlinSuspendingFunction(method, target, args); + } + + @API(status = INTERNAL, since = "6.1") + public static boolean isInstanceOfInlineType(@Nullable Object value) { + return jvmInline != null && value != null && value.getClass().getDeclaredAnnotation(jvmInline) != null; + } + + @API(status = INTERNAL, since = "6.1") + public static @Nullable Object invokeKotlinFunction(Method method, @Nullable Object target, + @Nullable Object... args) { + requireKotlinReflect(method); + return KotlinFunctionUtils.invokeKotlinFunction(method, target, args); } private static void requireKotlinReflect(Method method) { diff --git a/jupiter-tests/src/test/kotlin/org/junit/jupiter/api/kotlin/GenericInlineValueClassTests.kt b/jupiter-tests/src/test/kotlin/org/junit/jupiter/api/kotlin/GenericInlineValueClassTests.kt new file mode 100644 index 000000000000..ac5cd5ce1226 --- /dev/null +++ b/jupiter-tests/src/test/kotlin/org/junit/jupiter/api/kotlin/GenericInlineValueClassTests.kt @@ -0,0 +1,83 @@ +/* + * Copyright 2015-2025 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ +package org.junit.jupiter.api.kotlin + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource + +/** + * Tests for generic inline value classes. + * These work because they compile to Object in JVM, bypassing strict type validation. + */ +class GenericInlineValueClassTests { + @MethodSource("resultProvider") + @ParameterizedTest + fun testResult(result: Result) { + assertEquals("success", result.getOrThrow()) + } + + @MethodSource("multipleResultsProvider") + @ParameterizedTest + fun testMultipleResults( + result1: Result, + result2: Result + ) { + assertEquals("data", result1.getOrThrow()) + assertEquals(42, result2.getOrThrow()) + } + + @MethodSource("nullableResultProvider") + @ParameterizedTest + fun testNullableResult(result: Result?) { + assertEquals("test", result?.getOrNull()) + } + + @MethodSource("customGenericProvider") + @ParameterizedTest + fun testCustomGenericContainer(container: Container) { + assertEquals("content", container.value) + } + + companion object { + @JvmStatic + fun resultProvider() = + listOf( + Arguments.of(Result.success("success")) + ) + + @JvmStatic + fun multipleResultsProvider() = + listOf( + Arguments.of( + Result.success("data"), + Result.success(42) + ) + ) + + @JvmStatic + fun nullableResultProvider() = + listOf( + Arguments.of(Result.success("test")) + ) + + @JvmStatic + fun customGenericProvider() = + listOf( + Arguments.of(Container("content")) + ) + } +} + +@JvmInline +value class Container( + val value: T +) diff --git a/jupiter-tests/src/test/kotlin/org/junit/jupiter/api/kotlin/PrimitiveWrapperInlineValueClassTests.kt b/jupiter-tests/src/test/kotlin/org/junit/jupiter/api/kotlin/PrimitiveWrapperInlineValueClassTests.kt new file mode 100644 index 000000000000..fc6924b25340 --- /dev/null +++ b/jupiter-tests/src/test/kotlin/org/junit/jupiter/api/kotlin/PrimitiveWrapperInlineValueClassTests.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2015-2025 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ +package org.junit.jupiter.api.kotlin + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource + +/** + * Tests for primitive-wrapper inline value classes. + * + * Currently disabled: These fail because Kotlin compiles them to primitives + * (UInt→int, UserId→long), causing JUnit's type validation to fail before + * reaching the invocation logic. + * + * Supporting these would require modifications to JUnit's core type validation system. + * + * @see Issue #5081 + */ +@Disabled("Primitive-wrapper inline value classes are not yet supported") +class PrimitiveWrapperInlineValueClassTests { + @MethodSource("uintProvider") + @ParameterizedTest + fun testUInt(value: UInt) { + assertEquals(42u, value) + } + + @MethodSource("userIdProvider") + @ParameterizedTest + fun testUserId(userId: UserId) { + assertEquals(123L, userId.value) + } + + @MethodSource("emailProvider") + @ParameterizedTest + fun testEmail(email: Email) { + assertEquals("test@example.com", email.value) + } + + companion object { + @JvmStatic + fun uintProvider() = listOf(Arguments.of(42u)) + + @JvmStatic + fun userIdProvider() = listOf(Arguments.of(UserId(123L))) + + @JvmStatic + fun emailProvider() = listOf(Arguments.of(Email("test@example.com"))) + } +} + +@JvmInline +value class UserId( + val value: Long +) + +@JvmInline +value class Email( + val value: String +)