Skip to content

Commit c1e32b5

Browse files
Avoid reporting potentially misleading suppressed validation exception
In case of earlier failures, the validation exception is no longer reported as suppressed. Resolves #3054. --------- Co-authored-by: Marc Philipp <[email protected]>
1 parent 427a031 commit c1e32b5

File tree

9 files changed

+124
-21
lines changed

9 files changed

+124
-21
lines changed

documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M3.adoc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,11 +105,13 @@ to start reporting discovery issues.
105105
via a configuration parameter. Please also see the
106106
<<../user-guide/index.adoc#extensions-keeping-state-autocloseable-migration, migration note>>
107107
for third-party extensions wanting to support both JUnit 5.13 and earlier versions.
108-
109108
* `java.util.Locale` arguments are now converted according to the IETF BCP 47 language tag
110109
format. See the
111110
<<../user-guide/index.adoc#writing-tests-parameterized-tests-argument-conversion-implicit, User Guide>>
112111
for details.
112+
* Avoid reporting potentially misleading validation exception for `@ParameterizedClass`
113+
test classes and `@ParameterizedTest` methods as suppressed exception for earlier
114+
failures.
113115

114116
[[release-notes-5.13.0-M3-junit-vintage]]
115117
=== JUnit Vintage

junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ClassTemplateInvocationContextProvider.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ public interface ClassTemplateInvocationContextProvider extends Extension {
9090
* invoked; never {@code null}
9191
* @return a {@code Stream} of {@code ClassTemplateInvocationContext}
9292
* instances for the invocation of the class template; never {@code null}
93+
* @throws TemplateInvocationValidationException if a validation fails when
94+
* while providing or closing the {@link java.util.stream.Stream}.
9395
* @see #supportsClassTemplate
9496
* @see ExtensionContext
9597
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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 static org.apiguardian.api.API.Status.EXPERIMENTAL;
14+
15+
import org.apiguardian.api.API;
16+
import org.junit.platform.commons.JUnitException;
17+
18+
/**
19+
* {@code TemplateInvocationValidationException} is an exception thrown by a
20+
* {@link TestTemplateInvocationContextProvider} or
21+
* {@link ClassTemplateInvocationContextProvider} if a validation fails when
22+
* while providing or closing {@link java.util.stream.Stream} of invocation
23+
* contexts.
24+
*
25+
* @since 5.13
26+
*/
27+
@API(status = EXPERIMENTAL, since = "5.13")
28+
public class TemplateInvocationValidationException extends JUnitException {
29+
30+
private static final long serialVersionUID = 1L;
31+
32+
public TemplateInvocationValidationException(String message) {
33+
super(message);
34+
}
35+
}

junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/TestTemplateInvocationContextProvider.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ public interface TestTemplateInvocationContextProvider extends Extension {
8686
* to be invoked; never {@code null}
8787
* @return a {@code Stream} of {@code TestTemplateInvocationContext}
8888
* instances for the invocation of the test template method; never {@code null}
89+
* @throws TemplateInvocationValidationException if a validation fails when
90+
* while providing or closing the {@link java.util.stream.Stream}.
8991
* @see #supportsTestTemplate
9092
* @see ExtensionContext
9193
*/

junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TemplateExecutor.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@
1919

2020
import org.junit.jupiter.api.extension.Extension;
2121
import org.junit.jupiter.api.extension.ExtensionContext;
22+
import org.junit.jupiter.api.extension.TemplateInvocationValidationException;
2223
import org.junit.jupiter.engine.execution.JupiterEngineExecutionContext;
2324
import org.junit.jupiter.engine.extension.ExtensionRegistry;
25+
import org.junit.platform.commons.util.ExceptionUtils;
2426
import org.junit.platform.commons.util.Preconditions;
2527
import org.junit.platform.engine.TestDescriptor;
2628
import org.junit.platform.engine.UniqueId;
@@ -52,11 +54,24 @@ private void executeForProvider(P provider, AtomicInteger invocationIndex,
5254

5355
int initialValue = invocationIndex.get();
5456

55-
try (Stream<? extends C> stream = provideContexts(provider, extensionContext)) {
57+
Stream<? extends C> stream = provideContexts(provider, extensionContext);
58+
try {
5659
stream.forEach(invocationContext -> createInvocationTestDescriptor(invocationContext,
5760
invocationIndex.incrementAndGet()) //
5861
.ifPresent(testDescriptor -> execute(dynamicTestExecutor, testDescriptor)));
5962
}
63+
catch (Throwable t) {
64+
try {
65+
stream.close();
66+
}
67+
catch (TemplateInvocationValidationException ignore) {
68+
// ignore exceptions from close() to avoid masking the original failure
69+
}
70+
throw ExceptionUtils.throwAsUncheckedException(t);
71+
}
72+
finally {
73+
stream.close();
74+
}
6075

6176
Preconditions.condition(
6277
invocationIndex.get() != initialValue || mayReturnZeroContexts(provider, extensionContext),

junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedInvocationContextProvider.java

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import java.util.stream.Stream;
1818

1919
import org.junit.jupiter.api.extension.ExtensionContext;
20+
import org.junit.jupiter.api.extension.TemplateInvocationValidationException;
2021
import org.junit.jupiter.params.provider.Arguments;
2122
import org.junit.jupiter.params.provider.ArgumentsProvider;
2223
import org.junit.jupiter.params.provider.ArgumentsSource;
@@ -47,12 +48,20 @@ protected Stream<T> provideInvocationContexts(ExtensionContext extensionContext,
4748
invocationCount.incrementAndGet();
4849
return declarationContext.createInvocationContext(formatter, arguments, invocationCount.intValue());
4950
})
50-
.onClose(() ->
51-
Preconditions.condition(invocationCount.get() > 0 || declarationContext.isAllowingZeroInvocations(),
52-
() -> String.format("Configuration error: You must configure at least one set of arguments for this @%s", declarationContext.getAnnotationName())));
51+
.onClose(() -> validateInvokedAtLeastOnce(invocationCount.get(),declarationContext ));
5352
// @formatter:on
5453
}
5554

55+
private static <T> void validateInvokedAtLeastOnce(long invocationCount,
56+
ParameterizedDeclarationContext<T> declarationContext) {
57+
if (invocationCount == 0 && !declarationContext.isAllowingZeroInvocations()) {
58+
String message = String.format(
59+
"Configuration error: You must configure at least one set of arguments for this @%s",
60+
declarationContext.getAnnotationName());
61+
throw new TemplateInvocationValidationException(message);
62+
}
63+
}
64+
5665
private static List<ArgumentsSource> collectArgumentSources(ParameterizedDeclarationContext<?> declarationContext) {
5766
List<ArgumentsSource> argumentsSources = findRepeatableAnnotations(declarationContext.getAnnotatedElement(),
5867
ArgumentsSource.class);

jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedClassIntegrationTests.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import static org.junit.platform.testkit.engine.EventConditions.started;
4141
import static org.junit.platform.testkit.engine.EventConditions.test;
4242
import static org.junit.platform.testkit.engine.EventConditions.uniqueId;
43+
import static org.junit.platform.testkit.engine.TestExecutionResultConditions.instanceOf;
4344
import static org.junit.platform.testkit.engine.TestExecutionResultConditions.message;
4445
import static org.junit.platform.testkit.engine.TestExecutionResultConditions.suppressed;
4546

@@ -69,6 +70,7 @@
6970
import org.junit.jupiter.api.extension.AnnotatedElementContext;
7071
import org.junit.jupiter.api.extension.ExtensionContext;
7172
import org.junit.jupiter.api.extension.ParameterContext;
73+
import org.junit.jupiter.api.extension.TemplateInvocationValidationException;
7274
import org.junit.jupiter.engine.AbstractJupiterTestEngineTests;
7375
import org.junit.jupiter.engine.Constants;
7476
import org.junit.jupiter.engine.descriptor.ClassTemplateInvocationTestDescriptor;
@@ -368,8 +370,9 @@ void failsWhenInvocationIsRequiredButNoArgumentSetsAreProvided() {
368370
var results = executeTestsForClass(ForbiddenZeroInvocationsTestCase.class);
369371

370372
results.containerEvents().assertThatEvents() //
371-
.haveExactly(1, event(finishedWithFailure(message(
372-
"Configuration error: You must configure at least one set of arguments for this @ParameterizedClass"))));
373+
.haveExactly(1,
374+
event(finishedWithFailure(instanceOf(TemplateInvocationValidationException.class), message(
375+
"Configuration error: You must configure at least one set of arguments for this @ParameterizedClass"))));
373376
}
374377

375378
@Test

jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestExtensionTests.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import org.junit.jupiter.api.extension.ExecutableInvoker;
3737
import org.junit.jupiter.api.extension.ExtensionContext;
3838
import org.junit.jupiter.api.extension.MediaType;
39+
import org.junit.jupiter.api.extension.TemplateInvocationValidationException;
3940
import org.junit.jupiter.api.extension.TestInstances;
4041
import org.junit.jupiter.api.function.ThrowingConsumer;
4142
import org.junit.jupiter.api.parallel.ExecutionMode;
@@ -145,7 +146,7 @@ void throwsExceptionWhenParameterizedTestIsNotInvokedAtLeastOnce() {
145146
extensionContextWithAnnotatedTestMethod);
146147
// cause the stream to be evaluated
147148
stream.toArray();
148-
var exception = assertThrows(JUnitException.class, stream::close);
149+
var exception = assertThrows(TemplateInvocationValidationException.class, stream::close);
149150

150151
assertThat(exception).hasMessage(
151152
"Configuration error: You must configure at least one set of arguments for this @ParameterizedTest");

jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestIntegrationTests.java

Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
import static org.junit.jupiter.engine.discovery.JupiterUniqueIdBuilder.uniqueIdForTestTemplateMethod;
2525
import static org.junit.jupiter.params.converter.DefaultArgumentConverter.DEFAULT_LOCALE_CONVERSION_FORMAT_PROPERTY_NAME;
2626
import static org.junit.jupiter.params.provider.Arguments.arguments;
27-
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
2827
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectIteration;
2928
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectMethod;
3029
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectUniqueId;
@@ -88,6 +87,8 @@
8887
import org.junit.jupiter.api.extension.ParameterResolutionException;
8988
import org.junit.jupiter.api.extension.ParameterResolver;
9089
import org.junit.jupiter.api.extension.RegisterExtension;
90+
import org.junit.jupiter.api.extension.TemplateInvocationValidationException;
91+
import org.junit.jupiter.engine.AbstractJupiterTestEngineTests;
9192
import org.junit.jupiter.engine.JupiterTestEngine;
9293
import org.junit.jupiter.params.ParameterizedTestIntegrationTests.RepeatableSourcesTestCase.Action;
9394
import org.junit.jupiter.params.aggregator.AggregateWith;
@@ -112,8 +113,8 @@
112113
import org.junit.jupiter.params.support.ParameterDeclarations;
113114
import org.junit.platform.commons.PreconditionViolationException;
114115
import org.junit.platform.commons.util.ClassUtils;
115-
import org.junit.platform.engine.DiscoverySelector;
116116
import org.junit.platform.engine.TestDescriptor;
117+
import org.junit.platform.engine.TestExecutionResult;
117118
import org.junit.platform.testkit.engine.EngineExecutionResults;
118119
import org.junit.platform.testkit.engine.EngineTestKit;
119120
import org.junit.platform.testkit.engine.Event;
@@ -123,7 +124,7 @@
123124
/**
124125
* @since 5.0
125126
*/
126-
class ParameterizedTestIntegrationTests {
127+
class ParameterizedTestIntegrationTests extends AbstractJupiterTestEngineTests {
127128

128129
private final Locale originalLocale = Locale.getDefault(Locale.Category.FORMAT);
129130

@@ -395,7 +396,7 @@ void executesLifecycleMethods() {
395396
LifecycleTestCase.lifecycleEvents.clear();
396397
LifecycleTestCase.testMethods.clear();
397398

398-
var results = execute(selectClass(LifecycleTestCase.class));
399+
var results = executeTestsForClass(LifecycleTestCase.class);
399400
results.allEvents().assertThatEvents() //
400401
.haveExactly(1,
401402
event(test("test1"), displayName("[1] argument=foo"), finishedWithFailure(message("foo")))) //
@@ -456,8 +457,9 @@ void failsWhenInvocationIsRequiredButNoArgumentSetsAreProvided() {
456457
var results = execute(ZeroInvocationsTestCase.class, "testThatRequiresInvocations", String.class);
457458

458459
results.containerEvents().assertThatEvents() //
459-
.haveExactly(1, event(finishedWithFailure(message(
460-
"Configuration error: You must configure at least one set of arguments for this @ParameterizedTest"))));
460+
.haveExactly(1,
461+
event(finishedWithFailure(instanceOf(TemplateInvocationValidationException.class), message(
462+
"Configuration error: You must configure at least one set of arguments for this @ParameterizedTest"))));
461463
}
462464

463465
@Test
@@ -500,12 +502,20 @@ void executesWithIso639LocaleConversionFormat() {
500502
results.allEvents().assertStatistics(stats -> stats.started(4).succeeded(4));
501503
}
502504

503-
private EngineExecutionResults execute(DiscoverySelector... selectors) {
504-
return EngineTestKit.engine(new JupiterTestEngine()).selectors(selectors).execute();
505-
}
505+
@Test
506+
void reportsExceptionInStaticInitializersWithoutInvocationCountValidation() {
507+
var results = executeTestsForClass(ExceptionInStaticInitializerTestCase.class);
506508

507-
private EngineExecutionResults execute(Class<?> testClass, String methodName, Class<?>... methodParameterTypes) {
508-
return execute(selectMethod(testClass, methodName, ClassUtils.nullSafeToString(methodParameterTypes)));
509+
var failure = results.containerEvents().stream() //
510+
.filter(finishedWithFailure()::matches) //
511+
.findAny() //
512+
.orElseThrow();
513+
514+
var throwable = failure.getRequiredPayload(TestExecutionResult.class).getThrowable().orElseThrow();
515+
516+
assertThat(throwable) //
517+
.isInstanceOf(ExceptionInInitializerError.class) //
518+
.hasNoSuppressedExceptions();
509519
}
510520

511521
private EngineExecutionResults execute(Map<String, String> configurationParameters, Class<?> testClass,
@@ -520,6 +530,10 @@ private EngineExecutionResults execute(String methodName, Class<?>... methodPara
520530
return execute(TestCase.class, methodName, methodParameterTypes);
521531
}
522532

533+
private EngineExecutionResults execute(Class<?> testClass, String methodName, Class<?>... methodParameterTypes) {
534+
return executeTests(selectMethod(testClass, methodName, ClassUtils.nullSafeToString(methodParameterTypes)));
535+
}
536+
523537
/**
524538
* @since 5.4
525539
*/
@@ -947,7 +961,7 @@ void duplicateMethodNames() {
947961
// other words, we're not really testing the support for @RepeatedTest
948962
// and @TestFactory, but their presence also contributes to the bug
949963
// reported in #3001.
950-
ParameterizedTestIntegrationTests.this.execute(selectClass(DuplicateMethodNamesMethodSourceTestCase.class))//
964+
executeTestsForClass(DuplicateMethodNamesMethodSourceTestCase.class)//
951965
.testEvents()//
952966
.assertStatistics(stats -> stats.started(8).failed(0).finished(8));
953967
}
@@ -1366,7 +1380,7 @@ void closeAutoCloseableArgumentsAfterTestDespiteEarlyFailure() {
13661380
@Test
13671381
void executesTwoIterationsBasedOnIterationAndUniqueIdSelector() {
13681382
var methodId = uniqueIdForTestTemplateMethod(TestCase.class, "testWithThreeIterations(int)");
1369-
var results = execute(selectUniqueId(appendTestTemplateInvocationSegment(methodId, 3)),
1383+
var results = executeTests(selectUniqueId(appendTestTemplateInvocationSegment(methodId, 3)),
13701384
selectIteration(selectMethod(TestCase.class, "testWithThreeIterations", "int"), 1));
13711385

13721386
results.allEvents().assertThatEvents() //
@@ -2648,4 +2662,24 @@ void test(AutoCloseableArgument autoCloseable) {
26482662
}
26492663
}
26502664

2665+
static class ExceptionInStaticInitializerTestCase {
2666+
2667+
static {
2668+
//noinspection ConstantValue
2669+
if (true)
2670+
throw new RuntimeException("boom");
2671+
}
2672+
2673+
private static Stream<String> getArguments() {
2674+
return Stream.of("foo", "bar");
2675+
}
2676+
2677+
@ParameterizedTest
2678+
@MethodSource("getArguments")
2679+
void test(String value) {
2680+
fail("should not be called: " + value);
2681+
}
2682+
2683+
}
2684+
26512685
}

0 commit comments

Comments
 (0)