Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ repository on GitHub.
assertion.
* Stop reporting discovery issues for synthetic methods, particularly in conjunction with
Kotlin suspend functions.
* Fix support for test methods with the same signature as a package-private methods
declared in super classes in different packages.

[[release-notes-6.0.1-junit-jupiter-deprecations-and-breaking-changes]]
==== Deprecations and Breaking Changes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -323,11 +323,7 @@ private DiscoverySelector selectClass(List<Class<?>> classes) {
}

private DiscoverySelector selectMethod(List<Class<?>> classes, Method method) {
if (classes.size() == 1) {
return DiscoverySelectors.selectMethod(classes.get(0), method);
}
int lastIndex = classes.size() - 1;
return DiscoverySelectors.selectNestedMethod(classes.subList(0, lastIndex), classes.get(lastIndex), method);
return new DeclaredMethodSelector(classes, method);
}

static class DummyClassTemplateInvocationContext implements ClassTemplateInvocationContext {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* 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.engine.discovery;

import java.lang.reflect.Method;
import java.util.List;

import org.junit.platform.commons.util.Preconditions;
import org.junit.platform.engine.DiscoverySelector;
import org.junit.platform.engine.discovery.MethodSelector;

/**
* Jupiter-specific selector for methods, potentially in nested classes.
*
* <p>The important difference to {@link MethodSelector} is that this selector's
* {@link #equals(Object)} method takes into account the selected method's
* {@linkplain Method#getDeclaringClass() declaring class} to support cases
* where a package-private method is declared in a super class in a different
* package and a method with the same signature is declared in a subclass. In
* that case both methods should be discovered because the one declared in the
* subclass does <em>not</em> override the one in the super class.
*
* @since 6.0.1
*/
record DeclaredMethodSelector(List<Class<?>> testClasses, Method method) implements DiscoverySelector {
DeclaredMethodSelector {
Preconditions.notEmpty(testClasses, "testClasses must not be empty");
Preconditions.containsNoNullElements(testClasses, "testClasses must not contain null elements");
Preconditions.notNull(method, "method must not be null");
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* 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.engine.discovery;

import static org.junit.platform.commons.util.ReflectionUtils.isPackagePrivate;

import java.lang.reflect.Method;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.junit.platform.commons.PreconditionViolationException;
import org.junit.platform.commons.support.ReflectionSupport;
import org.junit.platform.commons.util.ClassUtils;
import org.junit.platform.commons.util.Preconditions;
import org.junit.platform.commons.util.ReflectionUtils;

/**
* @since 5.0
*/
class MethodSegmentResolver {

// Pattern: [declaringClassName#]methodName(comma-separated list of parameter type names)
private static final Pattern METHOD_PATTERN = Pattern.compile(
"(?:(?<declaringClass>.+)#)?(?<method>.+)\\((?<parameters>.*)\\)");

/**
* If the {@code method} is package-private and declared a class in a
* different package than {@code testClass}, the declaring class name is
* included in the method's unique ID segment. Otherwise, it only
* consists of the method name and its parameter types.
*/
String formatMethodSpecPart(Method method, Class<?> testClass) {
var parameterTypes = ClassUtils.nullSafeToString(method.getParameterTypes());
if (isPackagePrivate(method)
&& !method.getDeclaringClass().getPackageName().equals(testClass.getPackageName())) {
return "%s#%s(%s)".formatted(method.getDeclaringClass().getName(), method.getName(), parameterTypes);
}
return "%s(%s)".formatted(method.getName(), parameterTypes);
}

Optional<Method> findMethod(String methodSpecPart, Class<?> testClass) {
Matcher matcher = METHOD_PATTERN.matcher(methodSpecPart);

Preconditions.condition(matcher.matches(),
() -> "Method [%s] does not match pattern [%s]".formatted(methodSpecPart, METHOD_PATTERN));

Class<?> targetClass = testClass;
String declaringClass = matcher.group("declaringClass");
if (declaringClass != null) {
targetClass = ReflectionUtils.tryToLoadClass(declaringClass).getNonNullOrThrow(
cause -> new PreconditionViolationException(
"Could not load declaring class with name: " + declaringClass, cause));
}
String methodName = matcher.group("method");
String parameterTypeNames = matcher.group("parameters");
return ReflectionSupport.findMethod(targetClass, methodName, parameterTypeNames);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@
import org.junit.jupiter.engine.discovery.predicates.IsTestMethod;
import org.junit.jupiter.engine.discovery.predicates.IsTestTemplateMethod;
import org.junit.jupiter.engine.discovery.predicates.TestClassPredicates;
import org.junit.platform.commons.util.ClassUtils;
import org.junit.platform.engine.DiscoveryIssue;
import org.junit.platform.engine.DiscoveryIssue.Severity;
import org.junit.platform.engine.DiscoverySelector;
Expand All @@ -59,7 +58,7 @@
*/
class MethodSelectorResolver implements SelectorResolver {

private static final MethodFinder methodFinder = new MethodFinder();
private static final MethodSegmentResolver methodSegmentResolver = new MethodSegmentResolver();
private final Predicate<Class<?>> testClassPredicate;

private final JupiterConfiguration configuration;
Expand All @@ -84,6 +83,20 @@ public Resolution resolve(NestedMethodSelector selector, Context context) {
Match::exact);
}

@Override
public Resolution resolve(DiscoverySelector selector, Context context) {
if (selector instanceof DeclaredMethodSelector methodSelector) {
var testClasses = methodSelector.testClasses();
if (testClasses.size() == 1) {
return resolve(context, emptyList(), testClasses.get(0), methodSelector::method, Match::exact);
}
int lastIndex = testClasses.size() - 1;
return resolve(context, testClasses.subList(0, lastIndex), testClasses.get(lastIndex),
methodSelector::method, Match::exact);
}
return unresolved();
}

private Resolution resolve(Context context, List<Class<?>> enclosingClasses, Class<?> testClass,
Supplier<Method> methodSupplier,
BiFunction<TestDescriptor, Supplier<Set<? extends DiscoverySelector>>, Match> matchFactory) {
Expand Down Expand Up @@ -209,7 +222,7 @@ Optional<TestDescriptor> resolveUniqueIdIntoTestDescriptor(UniqueId uniqueId, Co
String methodSpecPart = lastSegment.getValue();
Class<?> testClass = ((TestClassAware) parent).getTestClass();
// @formatter:off
return methodFinder.findMethod(methodSpecPart, testClass)
return methodSegmentResolver.findMethod(methodSpecPart, testClass)
.filter(methodPredicate)
.map(method -> createTestDescriptor(parent, testClass, method, configuration));
// @formatter:on
Expand All @@ -223,15 +236,14 @@ Optional<TestDescriptor> resolveUniqueIdIntoTestDescriptor(UniqueId uniqueId, Co

private TestDescriptor createTestDescriptor(TestDescriptor parent, Class<?> testClass, Method method,
JupiterConfiguration configuration) {
UniqueId uniqueId = createUniqueId(method, parent);
UniqueId uniqueId = createUniqueId(method, parent, testClass);
return testDescriptorFactory.create(uniqueId, testClass, method,
((TestClassAware) parent)::getEnclosingTestClasses, configuration);
}

private UniqueId createUniqueId(Method method, TestDescriptor parent) {
String methodId = "%s(%s)".formatted(method.getName(),
ClassUtils.nullSafeToString(method.getParameterTypes()));
return parent.getUniqueId().append(segmentType, methodId);
private UniqueId createUniqueId(Method method, TestDescriptor parent, Class<?> testClass) {
return parent.getUniqueId().append(segmentType,
methodSegmentResolver.formatMethodSpecPart(method, testClass));
}

interface TestDescriptorFactory {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1872,7 +1872,11 @@ private static boolean isMethodOverriddenBy(Method upper, Method lower) {
return hasCompatibleSignature(upper, lower.getName(), lower.getParameterTypes());
}

private static boolean isPackagePrivate(Member member) {
/**
* @since 6.0.1
*/
@API(status = INTERNAL, since = "6.0.1")
public static boolean isPackagePrivate(Member member) {
int modifiers = member.getModifiers();
return !(Modifier.isPublic(modifiers) || Modifier.isProtected(modifiers) || Modifier.isPrivate(modifiers));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,44 +82,38 @@ public final class MethodSelector implements DiscoverySelector {
}

MethodSelector(Class<?> javaClass, String methodName, String parameterTypeNames) {
this.classLoader = javaClass.getClassLoader();
this(javaClass.getClassLoader(), javaClass.getName(), methodName, parameterTypeNames);
this.javaClass = javaClass;
this.className = javaClass.getName();
this.methodName = methodName;
this.parameterTypeNames = parameterTypeNames;
}

/**
* @since 1.10
*/
MethodSelector(@Nullable ClassLoader classLoader, String className, String methodName, Class<?>... parameterTypes) {
this.classLoader = classLoader;
this.className = className;
this.methodName = methodName;
this(classLoader, className, methodName, ClassUtils.nullSafeToString(Class::getTypeName, parameterTypes));
this.parameterTypes = parameterTypes.clone();
this.parameterTypeNames = ClassUtils.nullSafeToString(Class::getTypeName, this.parameterTypes);
}

/**
* @since 1.10
*/
MethodSelector(Class<?> javaClass, String methodName, Class<?>... parameterTypes) {
this.classLoader = javaClass.getClassLoader();
this(javaClass.getClassLoader(), javaClass.getName(), methodName,
ClassUtils.nullSafeToString(Class::getTypeName, parameterTypes));
this.javaClass = javaClass;
this.className = javaClass.getName();
this.methodName = methodName;
this.parameterTypes = parameterTypes.clone();
this.parameterTypeNames = ClassUtils.nullSafeToString(Class::getTypeName, this.parameterTypes);
}

MethodSelector(Class<?> javaClass, Method method) {
this.classLoader = javaClass.getClassLoader();
this(javaClass, method, method.getParameterTypes());
}

private MethodSelector(Class<?> javaClass, Method method, Class<?>... parameterTypes) {
this(javaClass.getClassLoader(), javaClass.getName(), method.getName(),
ClassUtils.nullSafeToString(Class::getTypeName, parameterTypes));
this.javaClass = javaClass;
this.className = javaClass.getName();
this.javaMethod = method;
this.methodName = method.getName();
this.parameterTypes = method.getParameterTypes();
this.parameterTypeNames = ClassUtils.nullSafeToString(Class::getTypeName, this.parameterTypes);
this.parameterTypes = parameterTypes;
}

/**
Expand Down
Loading