Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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

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 Down Expand Up @@ -209,7 +208,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 +222,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 @@ -64,6 +64,7 @@ public final class MethodSelector implements DiscoverySelector {
private final String className;
private final String methodName;
private final String parameterTypeNames;
private final @Nullable String declaringClassName;

private volatile @Nullable Class<?> javaClass;

Expand All @@ -75,51 +76,51 @@ public final class MethodSelector implements DiscoverySelector {
* @since 1.10
*/
MethodSelector(@Nullable ClassLoader classLoader, String className, String methodName, String parameterTypeNames) {
this.classLoader = classLoader;
this.className = className;
this.methodName = methodName;
this.parameterTypeNames = parameterTypeNames;
this(classLoader, className, methodName, parameterTypeNames, null);
}

MethodSelector(Class<?> javaClass, String methodName, String parameterTypeNames) {
this.classLoader = javaClass.getClassLoader();
this(javaClass.getClassLoader(), javaClass.getName(), methodName, parameterTypeNames, null);
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), null);
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), null);
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), method.getDeclaringClass().getName());
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;
}

private MethodSelector(@Nullable ClassLoader classLoader, String className, String methodName,
String parameterTypeNames, @Nullable String declaringClassName) {
this.classLoader = classLoader;
this.className = className;
this.methodName = methodName;
this.parameterTypeNames = parameterTypeNames;
this.declaringClassName = className.equals(declaringClassName) ? null : declaringClassName;
}

/**
Expand Down Expand Up @@ -277,7 +278,8 @@ public boolean equals(Object o) {
MethodSelector that = (MethodSelector) o;
return Objects.equals(this.className, that.className)//
&& Objects.equals(this.methodName, that.methodName)//
&& Objects.equals(this.parameterTypeNames, that.parameterTypeNames);
&& Objects.equals(this.parameterTypeNames, that.parameterTypeNames)//
&& Objects.equals(this.declaringClassName, that.declaringClassName);
}

/**
Expand All @@ -286,7 +288,7 @@ public boolean equals(Object o) {
@API(status = STABLE, since = "1.3")
@Override
public int hashCode() {
return Objects.hash(this.className, this.methodName, this.parameterTypeNames);
return Objects.hash(this.className, this.methodName, this.parameterTypeNames, this.declaringClassName);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* 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;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.platform.commons.util.CollectionUtils.getOnlyElement;
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectUniqueId;

import java.util.Map;
import java.util.stream.Stream;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.TestReporter;
import org.junit.jupiter.engine.subpackage.SuperClassWithPackagePrivateLifecycleMethodInDifferentPackageTestCase;
import org.junit.platform.engine.TestDescriptor;
import org.junit.platform.engine.reporting.ReportEntry;
import org.junit.platform.testkit.engine.EngineExecutionResults;

/**
* @since 6.0.1
*/
class TestMethodOverridingTests extends AbstractJupiterTestEngineTests {

@Test
void bothPackagePrivateTestMethodsAreDiscovered() {
var results = discoverTestsForClass(DuplicateTestMethodDueToPackagePrivateVisibilityTestCase.class);
var classDescriptor = getOnlyElement(results.getEngineDescriptor().getChildren());

assertThat(classDescriptor.getChildren()).hasSize(2);

var parentUniqueId = classDescriptor.getUniqueId();
var inheritedMethodUniqueId = parentUniqueId.append("method",
"org.junit.jupiter.engine.subpackage.SuperClassWithPackagePrivateLifecycleMethodInDifferentPackageTestCase#"
+ "test(org.junit.jupiter.api.TestInfo, org.junit.jupiter.api.TestReporter)");
var declaredMethodUniqueId = parentUniqueId.append("method",
"test(org.junit.jupiter.api.TestInfo, org.junit.jupiter.api.TestReporter)");

assertThat(classDescriptor.getChildren()) //
.extracting(TestDescriptor::getUniqueId) //
.containsExactly(inheritedMethodUniqueId, declaredMethodUniqueId);

results = discoverTests(selectUniqueId(inheritedMethodUniqueId));
classDescriptor = getOnlyElement(results.getEngineDescriptor().getChildren());
assertThat(classDescriptor.getChildren()) //
.extracting(TestDescriptor::getUniqueId) //
.containsExactly(inheritedMethodUniqueId);

results = discoverTests(selectUniqueId(declaredMethodUniqueId));
classDescriptor = getOnlyElement(results.getEngineDescriptor().getChildren());
assertThat(classDescriptor.getChildren()) //
.extracting(TestDescriptor::getUniqueId) //
.containsExactly(declaredMethodUniqueId);
}

@Test
void bothPackagePrivateTestMethodsAreExecuted() throws Exception {
var results = executeTestsForClass(DuplicateTestMethodDueToPackagePrivateVisibilityTestCase.class);

results.testEvents().assertStatistics(stats -> stats.started(2).succeeded(2));
assertThat(allReportEntries(results)) //
.containsExactly(
Map.of("invokedSuper",
SuperClassWithPackagePrivateLifecycleMethodInDifferentPackageTestCase.class.getDeclaredMethod(
"test", TestInfo.class, TestReporter.class).toGenericString()),
Map.of("invokedChild",
DuplicateTestMethodDueToPackagePrivateVisibilityTestCase.class.getDeclaredMethod("test",
TestInfo.class, TestReporter.class).toGenericString()));
}

private static Stream<Map<String, String>> allReportEntries(EngineExecutionResults results) {
return results.allEvents().reportingEntryPublished() //
.map(event -> event.getRequiredPayload(ReportEntry.class)) //
.map(ReportEntry::getKeyValuePairs);
}

@SuppressWarnings("JUnitMalformedDeclaration")
static class DuplicateTestMethodDueToPackagePrivateVisibilityTestCase
extends SuperClassWithPackagePrivateLifecycleMethodInDifferentPackageTestCase {

// @Override
@Test
void test(TestInfo testInfo, TestReporter reporter) {
reporter.publishEntry("invokedChild", testInfo.getTestMethod().orElseThrow().toGenericString());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.TestReporter;

/**
* @since 5.9
Expand All @@ -28,7 +30,8 @@ void beforeEach() {
}

@Test
void test() {
void test(TestInfo testInfo, TestReporter reporter) {
reporter.publishEntry("invokedSuper", testInfo.getTestMethod().orElseThrow().toGenericString());
assertThat(this.beforeEachInvoked).isTrue();
}

Expand Down
Loading