Skip to content

Commit 495ef75

Browse files
Ignore Kotlin compiler-generated DefaultImpls classes (#4696)
Resolves #4675. --------- Co-authored-by: Sam Brannen <[email protected]>
1 parent 3f28207 commit 495ef75

File tree

15 files changed

+210
-24
lines changed

15 files changed

+210
-24
lines changed

documentation/src/docs/asciidoc/release-notes/release-notes-5.13.3.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ repository on GitHub.
3737

3838
* Stop reporting discovery issues for composed annotation classes that are meta-annotated
3939
with `@Nested`.
40+
* Stop reporting discovery issues for `DefaultImpls` classes generated by the Kotlin
41+
compiler for interfaces with non-abstract test methods.
4042

4143
[[release-notes-5.13.3-junit-jupiter-deprecations-and-breaking-changes]]
4244
==== Deprecations and Breaking Changes

junit-jupiter-api/junit-jupiter-api.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ dependencies {
1818
compileOnly(kotlin("stdlib"))
1919

2020
testFixturesImplementation(libs.assertj)
21+
testFixturesImplementation(testFixtures(projects.junitPlatformCommons))
2122

2223
osgiVerification(projects.junitJupiterEngine)
2324
osgiVerification(projects.junitPlatformLauncher)
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright 2015-2025 the original author or authors.
3+
*
4+
* All rights reserved. This program and the accompanying materials are
5+
* made available under the terms of the Eclipse Public License v2.0 which
6+
* accompanies this distribution and is available at
7+
*
8+
* https://www.eclipse.org/legal/epl-v20.html
9+
*/
10+
11+
package org.junit.jupiter.api.extension;
12+
13+
import java.lang.annotation.ElementType;
14+
import java.lang.annotation.Retention;
15+
import java.lang.annotation.RetentionPolicy;
16+
import java.lang.annotation.Target;
17+
18+
import org.junit.jupiter.api.condition.DisabledIf;
19+
20+
/**
21+
* @see org.junit.platform.commons.test.IdeUtils#runningInEclipse()
22+
*/
23+
@Retention(RetentionPolicy.RUNTIME)
24+
@Target({ ElementType.TYPE, ElementType.METHOD })
25+
@DisabledIf("org.junit.platform.commons.test.IdeUtils#runningInEclipse()")
26+
public @interface DisabledInEclipse {
27+
String value() default "";
28+
}

junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/predicates/TestClassPredicates.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import static org.junit.platform.commons.support.ModifierSupport.isAbstract;
1616
import static org.junit.platform.commons.support.ModifierSupport.isNotAbstract;
1717
import static org.junit.platform.commons.support.ModifierSupport.isNotPrivate;
18+
import static org.junit.platform.commons.util.KotlinReflectionUtils.isKotlinInterfaceDefaultImplsClass;
1819
import static org.junit.platform.commons.util.ReflectionUtils.isInnerClass;
1920
import static org.junit.platform.commons.util.ReflectionUtils.isMethodPresent;
2021
import static org.junit.platform.commons.util.ReflectionUtils.isNestedClassPresent;
@@ -74,7 +75,7 @@ public boolean looksLikeIntendedTestClass(Class<?> candidate) {
7475
}
7576

7677
private boolean looksLikeIntendedTestClass(Class<?> candidate, Set<Class<?>> seen) {
77-
if (seen.add(candidate)) {
78+
if (seen.add(candidate) && !isKotlinInterfaceDefaultImplsClass(candidate)) {
7879
return this.isAnnotatedWithClassTemplate.test(candidate) //
7980
|| hasTestOrTestFactoryOrTestTemplateMethods(candidate) //
8081
|| hasNestedTests(candidate, seen);

junit-platform-commons/src/main/java/org/junit/platform/commons/util/KotlinReflectionUtils.java

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,16 @@
1111
package org.junit.platform.commons.util;
1212

1313
import static org.apiguardian.api.API.Status.INTERNAL;
14+
import static org.junit.platform.commons.util.ReflectionUtils.EMPTY_CLASS_ARRAY;
15+
import static org.junit.platform.commons.util.ReflectionUtils.findMethod;
16+
import static org.junit.platform.commons.util.ReflectionUtils.isStatic;
1417
import static org.junit.platform.commons.util.ReflectionUtils.tryToLoadClass;
1518

1619
import java.lang.annotation.Annotation;
1720
import java.lang.reflect.Method;
1821
import java.lang.reflect.Parameter;
1922
import java.lang.reflect.Type;
23+
import java.util.Arrays;
2024

2125
import org.apiguardian.api.API;
2226
import org.jspecify.annotations.Nullable;
@@ -25,15 +29,16 @@
2529
/**
2630
* Internal Kotlin-specific reflection utilities
2731
*
28-
* @since 6.0
32+
* @since 5.13.3
2933
*/
30-
@API(status = INTERNAL, since = "6.0")
34+
@API(status = INTERNAL, since = "5.13.3")
3135
public class KotlinReflectionUtils {
3236

3337
private static final @Nullable Class<? extends Annotation> kotlinMetadata;
3438
private static final @Nullable Class<?> kotlinCoroutineContinuation;
3539
private static final boolean kotlinReflectPresent;
3640
private static final boolean kotlinxCoroutinesPresent;
41+
public static final String DEFAULT_IMPLS_CLASS_NAME = "DefaultImpls";
3742

3843
static {
3944
var metadata = tryToLoadKotlinMetadataClass();
@@ -56,6 +61,10 @@ private static Try<Class<? extends Annotation>> tryToLoadKotlinMetadataClass() {
5661
.andThenTry(it -> (Class<? extends Annotation>) it);
5762
}
5863

64+
/**
65+
* @since 6.0
66+
*/
67+
@API(status = INTERNAL, since = "6.0")
5968
public static boolean isKotlinSuspendingFunction(Method method) {
6069
if (kotlinCoroutineContinuation != null && isKotlinType(method.getDeclaringClass())) {
6170
int parameterCount = method.getParameterCount();
@@ -65,6 +74,51 @@ public static boolean isKotlinSuspendingFunction(Method method) {
6574
return false;
6675
}
6776

77+
/**
78+
* Determines whether the supplied class is a {@code DefaultImpls} class
79+
* generated by the Kotlin compiler.
80+
*
81+
* <p>See
82+
* <a href="https://kotlinlang.org/docs/interfaces.html#jvm-default-method-generation-for-interface-functions">Kotlin documentation</a>
83+
* for details.
84+
*
85+
* @since 5.13.3
86+
*/
87+
@API(status = INTERNAL, since = "5.13.3")
88+
public static boolean isKotlinInterfaceDefaultImplsClass(Class<?> clazz) {
89+
if (!isKotlinType(clazz) || !DEFAULT_IMPLS_CLASS_NAME.equals(clazz.getSimpleName()) || !isStatic(clazz)) {
90+
return false;
91+
}
92+
93+
Class<?> enclosingClass = clazz.getEnclosingClass();
94+
if (enclosingClass != null && enclosingClass.isInterface()) {
95+
return Arrays.stream(clazz.getDeclaredMethods()) //
96+
.anyMatch(method -> isCompilerGeneratedDefaultMethod(method, enclosingClass));
97+
}
98+
99+
return false;
100+
}
101+
102+
private static boolean isCompilerGeneratedDefaultMethod(Method method, Class<?> enclosingClass) {
103+
if (isStatic(method) && method.getParameterCount() > 0) {
104+
var parameterTypes = method.getParameterTypes();
105+
if (parameterTypes[0] == enclosingClass) {
106+
var originalParameterTypes = copyWithoutFirst(parameterTypes);
107+
return findMethod(enclosingClass, method.getName(), originalParameterTypes).isPresent();
108+
}
109+
}
110+
return false;
111+
}
112+
113+
private static Class<?>[] copyWithoutFirst(Class<?>[] values) {
114+
if (values.length == 1) {
115+
return EMPTY_CLASS_ARRAY;
116+
}
117+
var result = new Class<?>[values.length - 1];
118+
System.arraycopy(values, 1, result, 0, result.length);
119+
return result;
120+
}
121+
68122
private static boolean isKotlinType(Class<?> clazz) {
69123
return kotlinMetadata != null //
70124
&& clazz.getDeclaredAnnotation(kotlinMetadata) != null;

junit-platform-commons/src/main/java/org/junit/platform/commons/util/KotlinSuspendingFunctionUtils.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import static kotlin.reflect.jvm.ReflectJvmMapping.getJavaType;
1919
import static kotlinx.coroutines.BuildersKt.runBlocking;
2020
import static org.junit.platform.commons.util.ExceptionUtils.throwAsUncheckedException;
21+
import static org.junit.platform.commons.util.ReflectionUtils.EMPTY_CLASS_ARRAY;
2122
import static org.junit.platform.commons.util.ReflectionUtils.getUnderlyingCause;
2223

2324
import java.lang.reflect.Method;
@@ -60,7 +61,7 @@ static Parameter[] getParameters(Method method) {
6061
static Class<?>[] getParameterTypes(Method method) {
6162
var parameterCount = method.getParameterCount();
6263
if (parameterCount == 1) {
63-
return new Class<?>[0];
64+
return EMPTY_CLASS_ARRAY;
6465
}
6566
return Arrays.stream(method.getParameterTypes()).limit(parameterCount - 1).toArray(Class<?>[]::new);
6667
}

junit-platform-commons/src/main/java/org/junit/platform/commons/util/ReflectionUtils.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ public enum HierarchyTraversalMode {
113113
// ++ => possessive quantifier
114114
private static final Pattern SOURCE_CODE_SYNTAX_ARRAY_PATTERN = Pattern.compile("^([^\\[\\]]+)((?>\\[\\])++)$");
115115

116-
private static final Class<?>[] EMPTY_CLASS_ARRAY = new Class<?>[0];
116+
static final Class<?>[] EMPTY_CLASS_ARRAY = new Class<?>[0];
117117

118118
private static final ClasspathScanner classpathScanner = ClasspathScannerLoader.getInstance();
119119

junit-vintage-engine/src/test/java/org/junit/vintage/engine/VintageTestEngineExecutionTests.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
import org.assertj.core.api.Condition;
4343
import org.junit.AssumptionViolatedException;
4444
import org.junit.jupiter.api.Test;
45-
import org.junit.jupiter.api.condition.DisabledIf;
45+
import org.junit.jupiter.api.extension.DisabledInEclipse;
4646
import org.junit.platform.commons.util.ReflectionUtils;
4747
import org.junit.platform.engine.EngineExecutionListener;
4848
import org.junit.platform.engine.ExecutionRequest;
@@ -865,7 +865,7 @@ void executesJUnit4TestCaseWithRunnerWithDuplicateChangingChildDescriptions() {
865865
}
866866

867867
@Test
868-
@DisabledIf("org.junit.platform.commons.test.IdeUtils#runningInEclipse()")
868+
@DisabledInEclipse
869869
void executesUnrolledSpockFeatureMethod() {
870870
// Load Groovy class via reflection to avoid compilation errors in Eclipse IDE.
871871
String testClassName = "org.junit.vintage.engine.samples.spock.SpockTestCaseWithUnrolledAndRegularFeatureMethods";
@@ -888,7 +888,7 @@ void executesUnrolledSpockFeatureMethod() {
888888
}
889889

890890
@Test
891-
@DisabledIf("org.junit.platform.commons.test.IdeUtils#runningInEclipse()")
891+
@DisabledInEclipse
892892
void executesRegularSpockFeatureMethod() {
893893
// Load Groovy class via reflection to avoid compilation errors in Eclipse IDE.
894894
String testClassName = "org.junit.vintage.engine.samples.spock.SpockTestCaseWithUnrolledAndRegularFeatureMethods";

jupiter-tests/src/test/java/org/junit/jupiter/engine/discovery/DiscoveryTests.java

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@
1414
import static org.assertj.core.api.Assertions.assertThat;
1515
import static org.junit.jupiter.api.Assertions.assertEquals;
1616
import static org.junit.jupiter.api.Assertions.fail;
17+
import static org.junit.jupiter.api.Assumptions.assumeFalse;
1718
import static org.junit.jupiter.api.Named.named;
1819
import static org.junit.jupiter.engine.discovery.JupiterUniqueIdBuilder.uniqueIdForTestTemplateMethod;
1920
import static org.junit.jupiter.params.provider.Arguments.argumentSet;
21+
import static org.junit.platform.commons.test.IdeUtils.runningInEclipse;
2022
import static org.junit.platform.commons.util.CollectionUtils.getOnlyElement;
2123
import static org.junit.platform.engine.discovery.ClassNameFilter.includeClassNamePatterns;
2224
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
@@ -39,6 +41,7 @@
3941
import org.junit.jupiter.api.Test;
4042
import org.junit.jupiter.api.TestInfo;
4143
import org.junit.jupiter.api.TestTemplate;
44+
import org.junit.jupiter.api.extension.DisabledInEclipse;
4245
import org.junit.jupiter.engine.AbstractJupiterTestEngineTests;
4346
import org.junit.jupiter.engine.JupiterTestEngine;
4447
import org.junit.jupiter.engine.descriptor.ClassTestDescriptor;
@@ -47,6 +50,7 @@
4750
import org.junit.jupiter.params.ParameterizedTest;
4851
import org.junit.jupiter.params.provider.Arguments;
4952
import org.junit.jupiter.params.provider.MethodSource;
53+
import org.junit.jupiter.params.provider.ValueSource;
5054
import org.junit.platform.engine.DiscoveryIssue;
5155
import org.junit.platform.engine.DiscoveryIssue.Severity;
5256
import org.junit.platform.engine.TestDescriptor;
@@ -74,6 +78,50 @@ void doNotDiscoverAbstractTestClass() {
7478
assertEquals(0, engineDescriptor.getDescendants().size(), "# resolved test descriptors");
7579
}
7680

81+
@ParameterizedTest
82+
@ValueSource(strings = { "org.junit.jupiter.engine.discovery.DiscoveryTests$InterfaceTestCase",
83+
"org.junit.jupiter.engine.kotlin.KotlinInterfaceTestCase" })
84+
void doNotDiscoverTestInterface(String className) {
85+
86+
assumeFalse(runningInEclipse() && className.contains(".kotlin."));
87+
88+
LauncherDiscoveryRequest request = defaultRequest().selectors(selectClass(className)).build();
89+
TestDescriptor engineDescriptor = discoverTestsWithoutIssues(request);
90+
assertEquals(0, engineDescriptor.getDescendants().size(), "# resolved test descriptors");
91+
}
92+
93+
@Test
94+
@DisabledInEclipse
95+
void doNotDiscoverGeneratedKotlinDefaultImplsClass() {
96+
LauncherDiscoveryRequest request = defaultRequest() //
97+
.selectors(selectClass("org.junit.jupiter.engine.kotlin.KotlinInterfaceTestCase$DefaultImpls")) //
98+
.build();
99+
TestDescriptor engineDescriptor = discoverTestsWithoutIssues(request);
100+
assertEquals(0, engineDescriptor.getDescendants().size(), "# resolved test descriptors");
101+
}
102+
103+
@Test
104+
@DisabledInEclipse
105+
void discoverDeclaredKotlinDefaultImplsClass() {
106+
LauncherDiscoveryRequest request = defaultRequest().selectors(
107+
selectClass("org.junit.jupiter.engine.kotlin.KotlinDefaultImplsTestCase$DefaultImpls")).build();
108+
TestDescriptor engineDescriptor = discoverTestsWithoutIssues(request);
109+
assertEquals(2, engineDescriptor.getDescendants().size(), "# resolved test descriptors");
110+
}
111+
112+
@ParameterizedTest
113+
@ValueSource(strings = {
114+
"org.junit.jupiter.engine.discovery.DiscoveryTests$ConcreteImplementationOfInterfaceTestCase",
115+
"org.junit.jupiter.engine.kotlin.KotlinInterfaceImplementationTestCase" })
116+
void discoverTestClassInheritingTestsFromInterface(String className) {
117+
118+
assumeFalse(runningInEclipse() && className.contains(".kotlin."));
119+
120+
LauncherDiscoveryRequest request = defaultRequest().selectors(selectClass(className)).build();
121+
TestDescriptor engineDescriptor = discoverTestsWithoutIssues(request);
122+
assertEquals(2, engineDescriptor.getDescendants().size(), "# resolved test descriptors");
123+
}
124+
77125
@Test
78126
void discoverMethodByUniqueId() {
79127
LauncherDiscoveryRequest request = defaultRequest().selectors(
@@ -524,4 +572,13 @@ void test() {
524572
}
525573
}
526574

575+
interface InterfaceTestCase {
576+
@Test
577+
default void test() {
578+
}
579+
}
580+
581+
static class ConcreteImplementationOfInterfaceTestCase implements InterfaceTestCase {
582+
}
583+
527584
}

jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TimeoutInvocationFactoryTests.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import org.junit.jupiter.api.DisplayName;
1919
import org.junit.jupiter.api.Test;
2020
import org.junit.jupiter.api.Timeout.ThreadMode;
21-
import org.junit.jupiter.api.condition.DisabledIf;
21+
import org.junit.jupiter.api.extension.DisabledInEclipse;
2222
import org.junit.jupiter.api.extension.ExtendWith;
2323
import org.junit.jupiter.api.extension.ExtensionContext.Store;
2424
import org.junit.jupiter.api.extension.InvocationInterceptor.Invocation;
@@ -35,9 +35,7 @@
3535
// Mockito cannot mock this class: class org.junit.jupiter.engine.execution.NamespaceAwareStore.
3636
// You are seeing this disclaimer because Mockito is configured to create inlined mocks.
3737
// Byte Buddy could not instrument all classes within the mock's type hierarchy.
38-
@DisabledIf(//
39-
value = "org.junit.platform.commons.test.IdeUtils#runningInEclipse()", //
40-
disabledReason = "Mockito cannot create a spy for NamespaceAwareStore using the inline MockMaker in Eclipse IDE")
38+
@DisabledInEclipse("Mockito cannot create a spy for NamespaceAwareStore using the inline MockMaker in Eclipse IDE")
4139
@DisplayName("TimeoutInvocationFactory")
4240
@ExtendWith(MockitoExtension.class)
4341
class TimeoutInvocationFactoryTests {

0 commit comments

Comments
 (0)