Skip to content

Report discovery issues for @Suite classes #4429

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Mar 31, 2025
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -566,10 +566,11 @@ private static class LifecycleMethods {
LifecycleMethods(ClassInfo classInfo) {
Class<?> testClass = classInfo.testClass;
boolean requireStatic = classInfo.lifecycle == Lifecycle.PER_METHOD;
this.beforeAll = findBeforeAllMethods(testClass, requireStatic, discoveryIssues::add);
this.afterAll = findAfterAllMethods(testClass, requireStatic, discoveryIssues::add);
this.beforeEach = findBeforeEachMethods(testClass, discoveryIssues::add);
this.afterEach = findAfterEachMethods(testClass, discoveryIssues::add);
DiscoveryIssueReporter issueReporter = DiscoveryIssueReporter.collecting(discoveryIssues);
this.beforeAll = findBeforeAllMethods(testClass, requireStatic, issueReporter);
this.afterAll = findAfterAllMethods(testClass, requireStatic, issueReporter);
this.beforeEach = findBeforeEachMethods(testClass, issueReporter);
this.afterEach = findAfterEachMethods(testClass, issueReporter);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import static org.junit.platform.commons.support.AnnotationSupport.findAnnotatedMethods;
import static org.junit.platform.commons.support.AnnotationSupport.findAnnotation;
import static org.junit.platform.commons.util.CollectionUtils.toUnmodifiableList;
import static org.junit.platform.engine.support.discovery.DiscoveryIssueReporter.Condition.alwaysSatisfied;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
Expand Down Expand Up @@ -93,8 +94,8 @@ static void validateClassTemplateInvocationLifecycleMethodsAreDeclaredCorrectly(
.and(requireStatic
? isStatic(issueReporter,
LifecycleMethodUtils::classTemplateInvocationLifecycleMethodAnnotationName)
: __ -> true) //
);
: alwaysSatisfied()) //
.toConsumer());
}

private static Stream<Method> findAllClassTemplateInvocationLifecycleMethods(Class<?> testClass) {
Expand All @@ -113,7 +114,7 @@ private static List<Method> findMethodsAndCheckStatic(Class<?> testClass, boolea

Condition<Method> additionalCondition = requireStatic
? isStatic(issueReporter, __ -> annotationType.getSimpleName())
: __ -> true;
: alwaysSatisfied();
return findMethodsAndCheckVoidReturnType(testClass, annotationType, traversalMode, issueReporter,
additionalCondition);
}
Expand All @@ -131,9 +132,9 @@ private static List<Method> findMethodsAndCheckVoidReturnType(Class<?> testClass
DiscoveryIssueReporter issueReporter, Condition<? super Method> additionalCondition) {

return findAnnotatedMethods(testClass, annotationType, traversalMode).stream() //
.peek(isNotPrivateWarning(issueReporter, annotationType::getSimpleName)) //
.filter(
returnsPrimitiveVoid(issueReporter, __ -> annotationType.getSimpleName()).and(additionalCondition)) //
.peek(isNotPrivateWarning(issueReporter, annotationType::getSimpleName).toConsumer()) //
.filter(returnsPrimitiveVoid(issueReporter, __ -> annotationType.getSimpleName()).and(
additionalCondition).toPredicate()) //
.collect(toUnmodifiableList());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ abstract class IsTestableMethod implements Predicate<Method> {
@Override
public boolean test(Method candidate) {
if (isAnnotated(candidate, this.annotationType)) {
return condition.test(candidate);
return condition.check(candidate);
}
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@
import static org.apiguardian.api.API.Status.EXPERIMENTAL;

import java.util.Optional;
import java.util.function.UnaryOperator;

import org.apiguardian.api.API;
import org.junit.platform.commons.util.Preconditions;

/**
* {@code DiscoveryIssue} represents an issue that was encountered during test
Expand All @@ -29,6 +31,8 @@ public interface DiscoveryIssue {
* Create a new {@code DiscoveryIssue} with the supplied {@link Severity} and
* message.
*
* @param severity the severity of the issue; never {@code null}
* @param message the message of the issue; never blank
* @see #builder(Severity, String)
*/
static DiscoveryIssue create(Severity severity, String message) {
Expand All @@ -39,10 +43,14 @@ static DiscoveryIssue create(Severity severity, String message) {
* Create a new {@link Builder} for creating a {@code DiscoveryIssue} with
* the supplied {@link Severity} and message.
*
* @param severity the severity of the issue; never {@code null}
* @param message the message of the issue; never blank
* @see Builder
* @see #create(Severity, String)
*/
static Builder builder(Severity severity, String message) {
Preconditions.notNull(severity, "severity must not be null");
Preconditions.notBlank(message, "message must not be blank");
return new DefaultDiscoveryIssue.Builder(severity, message);
}

Expand All @@ -66,6 +74,22 @@ static Builder builder(Severity severity, String message) {
*/
Optional<Throwable> cause();

/**
* Create a copy of this issue with the modified message produced by the
* supplied operator.
*/
default DiscoveryIssue withMessage(UnaryOperator<String> messageModifier) {
String oldMessage = message();
String newMessage = messageModifier.apply(oldMessage);
if (oldMessage.equals(newMessage)) {
return this;
}
return DiscoveryIssue.builder(severity(), newMessage) //
.source(source()) //
.cause(cause()) //
.build();
}

/**
* The severity of a {@code DiscoveryIssue}.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import static org.apiguardian.api.API.Status.EXPERIMENTAL;

import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Consumer;
Expand All @@ -28,11 +29,12 @@
* {@code DiscoveryIssueReporter} defines the API for reporting
* {@link DiscoveryIssue DiscoveryIssues}.
*
* <p>This interface is not intended to be implemented by clients.
*
* @since 1.13
* @see SelectorResolver.Context
*/
@API(status = EXPERIMENTAL, since = "1.13")
@FunctionalInterface
public interface DiscoveryIssueReporter {

/**
Expand All @@ -49,6 +51,28 @@ static DiscoveryIssueReporter forwarding(EngineDiscoveryListener engineDiscovery
return issue -> engineDiscoveryListener.issueEncountered(engineId, issue);
}

/**
* Create a new {@code DiscoveryIssueReporter} that adds reported issues to
* the supplied collection.
*
* @param collection the collection to add issues to; never {@code null}
*/
static DiscoveryIssueReporter collecting(Collection<? super DiscoveryIssue> collection) {
Preconditions.notNull(collection, "collection must not be null");
return collection::add;
}

/**
* Create a new {@code DiscoveryIssueReporter} that adds reported issues to
* the supplied consumer.
*
* @param consumer the consumer to report issues to; never {@code null}
*/
static DiscoveryIssueReporter consuming(Consumer<? super DiscoveryIssue> consumer) {
Preconditions.notNull(consumer, "consumer must not be null");
return consumer::accept;
}

/**
* Create a new {@code DiscoveryIssueReporter} that avoids reporting
* duplicate issues.
Expand Down Expand Up @@ -113,10 +137,23 @@ default <T> Condition<T> createReportingCondition(Predicate<T> predicate,
* for filtering, or to {@link java.util.stream.Stream#peek(Consumer)} if it
* is only used for reporting or other side effects.
*
* <p>This interface is not intended to be implemented by clients.
*
* @see #createReportingCondition(Predicate, Function)
*/
@FunctionalInterface
interface Condition<T> extends Predicate<T>, Consumer<T> {
interface Condition<T> {

/**
* Create a {@link Condition} that is always satisfied.
*/
static <T> Condition<T> alwaysSatisfied() {
return __ -> true;
}

/**
* Evaluate this condition to potentially report an issue.
*/
boolean check(T value);

/**
* Return a composed condition that represents a logical AND of this
Expand All @@ -128,35 +165,24 @@ interface Condition<T> extends Predicate<T>, Consumer<T> {
*
* @return the composed condition; never {@code null}
*/
@Override
default Condition<T> and(Predicate<? super T> other) {
Preconditions.notNull(other, "other condition must not be null");
return (t) -> test(t) & other.test(t);
default Condition<T> and(Condition<? super T> that) {
Preconditions.notNull(that, "condition must not be null");
return value -> this.check(value) & that.check(value);
}

/**
* Return a composed condition that represents a logical AND of this
* and the supplied condition.
*
* <p>The default implementation avoids short-circuiting so
* <em>both</em> conditions will be evaluated even if this condition
* returns {@code true} to ensure that all issues are reported.
*
* @return the composed condition; never {@code null}
* {@return this condition as a {@link Predicate}}
*/
@Override
default Predicate<T> or(Predicate<? super T> other) {
Preconditions.notNull(other, "other condition must not be null");
return (t) -> test(t) | other.test(t);
default Predicate<T> toPredicate() {
return this::check;
}

/**
* Evaluate the {@code #test(Object)} method of this condition to
* potentially report an issue.
* {@return this condition as a {@link Consumer}}
*/
@Override
default void accept(T value) {
test(value);
default Consumer<T> toConsumer() {
return this::check;
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
package org.junit.platform.suite.commons;

import static java.util.stream.Collectors.toList;
import static org.apiguardian.api.API.Status.INTERNAL;
import static org.junit.platform.commons.support.AnnotationSupport.findAnnotation;
import static org.junit.platform.commons.support.AnnotationSupport.findRepeatableAnnotations;
import static org.junit.platform.engine.discovery.ClassNameFilter.STANDARD_INCLUDE_PATTERN;
Expand All @@ -30,7 +31,6 @@
import java.util.stream.Stream;

import org.apiguardian.api.API;
import org.apiguardian.api.API.Status;
import org.junit.platform.commons.util.Preconditions;
import org.junit.platform.commons.util.StringUtils;
import org.junit.platform.engine.ConfigurationParameters;
Expand All @@ -43,6 +43,7 @@
import org.junit.platform.engine.discovery.PackageNameFilter;
import org.junit.platform.engine.reporting.OutputDirectoryProvider;
import org.junit.platform.launcher.EngineFilter;
import org.junit.platform.launcher.LauncherDiscoveryListener;
import org.junit.platform.launcher.LauncherDiscoveryRequest;
import org.junit.platform.launcher.TagFilter;
import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
Expand Down Expand Up @@ -111,8 +112,7 @@
* @see org.junit.platform.launcher.EngineFilter
* @see org.junit.platform.launcher.TagFilter
*/
@API(status = Status.INTERNAL, since = "1.8", consumers = { "org.junit.platform.suite.engine",
"org.junit.platform.runner" })
@API(status = INTERNAL, since = "1.8", consumers = { "org.junit.platform.suite.engine", "org.junit.platform.runner" })
public final class SuiteLauncherDiscoveryRequestBuilder {

private final LauncherDiscoveryRequestBuilder delegate = LauncherDiscoveryRequestBuilder.request();
Expand Down Expand Up @@ -268,6 +268,12 @@ public SuiteLauncherDiscoveryRequestBuilder outputDirectoryProvider(
return this;
}

@API(status = INTERNAL, since = "1.13")
public SuiteLauncherDiscoveryRequestBuilder listener(LauncherDiscoveryListener listener) {
delegate.listeners(listener);
return this;
}

/**
* Apply a suite's annotation-based configuration, selectors, and filters to
* this builder.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,18 @@
import java.util.Optional;
import java.util.function.Predicate;

import org.junit.platform.commons.logging.Logger;
import org.junit.platform.commons.logging.LoggerFactory;
import org.junit.platform.commons.support.ReflectionSupport;
import org.junit.platform.engine.ConfigurationParameters;
import org.junit.platform.engine.DiscoveryIssue;
import org.junit.platform.engine.DiscoveryIssue.Severity;
import org.junit.platform.engine.EngineDiscoveryListener;
import org.junit.platform.engine.TestDescriptor;
import org.junit.platform.engine.UniqueId;
import org.junit.platform.engine.UniqueId.Segment;
import org.junit.platform.engine.discovery.ClassSelector;
import org.junit.platform.engine.discovery.UniqueIdSelector;
import org.junit.platform.engine.reporting.OutputDirectoryProvider;
import org.junit.platform.engine.support.descriptor.ClassSource;
import org.junit.platform.engine.support.discovery.DiscoveryIssueReporter;
import org.junit.platform.engine.support.discovery.SelectorResolver;

Expand All @@ -34,23 +36,24 @@
*/
final class ClassSelectorResolver implements SelectorResolver {

private static final Logger log = LoggerFactory.getLogger(ClassSelectorResolver.class);

private static final IsSuiteClass isSuiteClass = new IsSuiteClass();

private final IsSuiteClass isSuiteClass;
private final Predicate<String> classNameFilter;
private final SuiteEngineDescriptor suiteEngineDescriptor;
private final ConfigurationParameters configurationParameters;
private final OutputDirectoryProvider outputDirectoryProvider;
private final EngineDiscoveryListener discoveryListener;
private final DiscoveryIssueReporter issueReporter;

ClassSelectorResolver(Predicate<String> classNameFilter, SuiteEngineDescriptor suiteEngineDescriptor,
ConfigurationParameters configurationParameters, OutputDirectoryProvider outputDirectoryProvider,
DiscoveryIssueReporter issueReporter) {
EngineDiscoveryListener discoveryListener, DiscoveryIssueReporter issueReporter) {

this.isSuiteClass = new IsSuiteClass(issueReporter);
this.classNameFilter = classNameFilter;
this.suiteEngineDescriptor = suiteEngineDescriptor;
this.configurationParameters = configurationParameters;
this.outputDirectoryProvider = outputDirectoryProvider;
this.discoveryListener = discoveryListener;
this.issueReporter = issueReporter;
}

Expand Down Expand Up @@ -106,12 +109,14 @@ private static Resolution toResolution(Optional<SuiteTestDescriptor> suite) {
private Optional<SuiteTestDescriptor> newSuiteDescriptor(Class<?> suiteClass, TestDescriptor parent) {
UniqueId id = parent.getUniqueId().append(SuiteTestDescriptor.SEGMENT_TYPE, suiteClass.getName());
if (containsCycle(id)) {
log.config(() -> createConfigContainsCycleMessage(suiteClass, id));
issueReporter.reportIssue(
DiscoveryIssue.builder(Severity.INFO, createConfigContainsCycleMessage(suiteClass, id)) //
.source(ClassSource.from(suiteClass)));
return Optional.empty();
}

return Optional.of(
new SuiteTestDescriptor(id, suiteClass, configurationParameters, outputDirectoryProvider, issueReporter));
return Optional.of(new SuiteTestDescriptor(id, suiteClass, configurationParameters, outputDirectoryProvider,
discoveryListener, issueReporter));
}

private static boolean containsCycle(UniqueId id) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import org.junit.platform.engine.EngineDiscoveryRequest;
import org.junit.platform.engine.TestDescriptor;
import org.junit.platform.engine.support.discovery.DiscoveryIssueReporter;
import org.junit.platform.engine.support.discovery.EngineDiscoveryRequestResolver;

/**
Expand All @@ -21,12 +22,13 @@ final class DiscoverySelectorResolver {

// @formatter:off
private static final EngineDiscoveryRequestResolver<SuiteEngineDescriptor> resolver = EngineDiscoveryRequestResolver.<SuiteEngineDescriptor>builder()
.addClassContainerSelectorResolver(new IsSuiteClass())
.addClassContainerSelectorResolverWithContext(context -> new IsSuiteClass(context.getIssueReporter()))
.addSelectorResolver(context -> new ClassSelectorResolver(
context.getClassNameFilter(),
context.getEngineDescriptor(),
context.getDiscoveryRequest().getConfigurationParameters(),
context.getDiscoveryRequest().getOutputDirectoryProvider(),
context.getDiscoveryRequest().getDiscoveryListener(),
context.getIssueReporter()))
.build();
// @formatter:on
Expand All @@ -40,7 +42,9 @@ private static void discoverSuites(SuiteEngineDescriptor engineDescriptor) {
}

void resolveSelectors(EngineDiscoveryRequest request, SuiteEngineDescriptor engineDescriptor) {
resolver.resolve(request, engineDescriptor);
DiscoveryIssueReporter issueReporter = DiscoveryIssueReporter.deduplicating(
DiscoveryIssueReporter.forwarding(request.getDiscoveryListener(), engineDescriptor.getUniqueId()));
resolver.resolve(request, engineDescriptor, issueReporter);
discoverSuites(engineDescriptor);
engineDescriptor.accept(TestDescriptor::prune);
}
Expand Down
Loading