From a058571c55fe6207b88eda01f9f6c1dba9954ed3 Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Tue, 28 Oct 2025 13:02:12 +0100 Subject: [PATCH] Stop reporting discovery issues for synthetic methods Prior to this commit, _all_ methods of a potential test class were checked for being test methods, including synthetic ones. However, when resolving a test class only non-synthetic methods were taken into account. Since the Kotlin compiler generates a synthetic, static method suffixed with `$suspendImpl` for every open (i.e. non-final) method and copies its annotations, such methods were included in the check. In addition to filtering out synthetic methods from being checked, `KotlinReflectionUtils` no longer recognizes them as suspend functions. Fixes #5102. --- .../release-notes/release-notes-6.0.1.adoc | 2 + .../predicates/IsTestableMethod.java | 2 +- .../commons/util/KotlinReflectionUtils.java | 2 +- .../api/kotlin/KotlinSuspendFunctionsTests.kt | 15 ++++++ platform-tests/platform-tests.gradle.kts | 3 +- .../util/KotlinReflectionUtilsTests.kt | 54 +++++++++++++++++++ 6 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 platform-tests/src/test/kotlin/org/junit/platform/commons/util/KotlinReflectionUtilsTests.kt diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-6.0.1.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-6.0.1.adoc index af78da2bc298..d5fbb3c02d43 100644 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-6.0.1.adoc +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-6.0.1.adoc @@ -45,6 +45,8 @@ repository on GitHub. to avoid conflicts with other control characters. * Fix `IllegalAccessError` thrown when using the Kotlin-specific `assertDoesNotThrow` assertion. +* Stop reporting discovery issues for synthetic methods, particularly in conjunction with + Kotlin suspend functions. [[release-notes-6.0.1-junit-jupiter-deprecations-and-breaking-changes]] ==== Deprecations and Breaking Changes diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/predicates/IsTestableMethod.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/predicates/IsTestableMethod.java index 7e95f77f8307..cb54c7b2020e 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/predicates/IsTestableMethod.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/predicates/IsTestableMethod.java @@ -45,7 +45,7 @@ abstract class IsTestableMethod implements Predicate { @Override public boolean test(Method candidate) { - if (isAnnotated(candidate, this.annotationType)) { + if (!candidate.isSynthetic() && isAnnotated(candidate, this.annotationType)) { return condition.check(candidate) && isNotAbstract(candidate); } return false; 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 626f3247c552..f3a04fe2e1a7 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 @@ -67,7 +67,7 @@ private static Try> tryToLoadKotlinMetadataClass() { */ @API(status = INTERNAL, since = "6.0") public static boolean isKotlinSuspendingFunction(Method method) { - if (kotlinCoroutineContinuation != null && isKotlinType(method.getDeclaringClass())) { + if (!method.isSynthetic() && kotlinCoroutineContinuation != null && isKotlinType(method.getDeclaringClass())) { int parameterCount = method.getParameterCount(); return parameterCount > 0 // && method.getParameterTypes()[parameterCount - 1] == kotlinCoroutineContinuation; diff --git a/jupiter-tests/src/test/kotlin/org/junit/jupiter/api/kotlin/KotlinSuspendFunctionsTests.kt b/jupiter-tests/src/test/kotlin/org/junit/jupiter/api/kotlin/KotlinSuspendFunctionsTests.kt index 9c784f9c1270..3410a191101c 100644 --- a/jupiter-tests/src/test/kotlin/org/junit/jupiter/api/kotlin/KotlinSuspendFunctionsTests.kt +++ b/jupiter-tests/src/test/kotlin/org/junit/jupiter/api/kotlin/KotlinSuspendFunctionsTests.kt @@ -41,6 +41,13 @@ class KotlinSuspendFunctionsTests : AbstractJupiterTestEngineTests() { assertThat(getPublishedEvents(results)).containsExactly("test") } + @Test + fun suspendingOpenTestMethodsAreSupported() { + val results = executeTestsForClass(OpenTestMethodTestCase::class) + assertAllTestsPassed(results, 1) + assertThat(getPublishedEvents(results)).containsExactly("test") + } + @Test fun suspendingTestTemplateMethodsAreSupported() { val results = executeTestsForClass(TestTemplateTestCase::class) @@ -187,6 +194,14 @@ class KotlinSuspendFunctionsTests : AbstractJupiterTestEngineTests() { } } + @Suppress("JUnitMalformedDeclaration") + open class OpenTestMethodTestCase { + @Test // https://github.com/junit-team/junit-framework/issues/5102 + open suspend fun `check getRandomPositiveInt`(reporter: TestReporter) { + suspendingPublish(reporter, "test") + } + } + @Suppress("RedundantSuspendModifier") companion object { suspend fun suspendingPublish( diff --git a/platform-tests/platform-tests.gradle.kts b/platform-tests/platform-tests.gradle.kts index 115662798ad0..9ea2c04a8531 100644 --- a/platform-tests/platform-tests.gradle.kts +++ b/platform-tests/platform-tests.gradle.kts @@ -5,7 +5,7 @@ import org.gradle.plugins.ide.eclipse.model.Classpath import org.gradle.plugins.ide.eclipse.model.SourceFolder plugins { - id("junitbuild.java-library-conventions") + id("junitbuild.kotlin-library-conventions") id("junitbuild.junit4-compatibility") id("junitbuild.testing-conventions") id("junitbuild.jmh-conventions") @@ -54,6 +54,7 @@ dependencies { testImplementation(libs.openTestReporting.tooling.core) testImplementation(libs.picocli) testImplementation(libs.bundles.xmlunit) + testImplementation(kotlin("stdlib")) testImplementation(testFixtures(projects.junitJupiterApi)) testImplementation(testFixtures(projects.junitPlatformReporting)) testImplementation(projects.platformTests) { diff --git a/platform-tests/src/test/kotlin/org/junit/platform/commons/util/KotlinReflectionUtilsTests.kt b/platform-tests/src/test/kotlin/org/junit/platform/commons/util/KotlinReflectionUtilsTests.kt new file mode 100644 index 000000000000..ccb00fec49b0 --- /dev/null +++ b/platform-tests/src/test/kotlin/org/junit/platform/commons/util/KotlinReflectionUtilsTests.kt @@ -0,0 +1,54 @@ +/* + * 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.platform.commons.util + +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.platform.commons.support.ModifierSupport +import kotlin.coroutines.Continuation + +class KotlinReflectionUtilsTests { + @Test + fun recognizesSuspendFunction() { + val method = + OpenTestMethodTestCase::class.java.getDeclaredMethod( + "test", + Continuation::class.java + ) + + assertFalse(ModifierSupport.isStatic(method)) + assertFalse(method.isSynthetic) + + assertTrue(KotlinReflectionUtils.isKotlinSuspendingFunction(method)) + } + + @Test + fun doesNotRecognizeSyntheticMethodAsSuspendFunction() { + val method = + OpenTestMethodTestCase::class.java.getDeclaredMethod( + "test\$suspendImpl", + OpenTestMethodTestCase::class.java, + Continuation::class.java + ) + + assertTrue(ModifierSupport.isStatic(method)) + assertTrue(method.isSynthetic) + + assertFalse(KotlinReflectionUtils.isKotlinSuspendingFunction(method)) + } + + @Suppress("JUnitMalformedDeclaration") + open class OpenTestMethodTestCase { + @Test + open suspend fun test() { + } + } +}