Skip to content

Commit 47291a3

Browse files
committed
Propagate errors from callbacks
1 parent bc03ecb commit 47291a3

File tree

2 files changed

+97
-23
lines changed

2 files changed

+97
-23
lines changed

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ public SkipResult shouldBeSkipped(JupiterEngineExecutionContext context) {
139139
public JupiterEngineExecutionContext before(JupiterEngineExecutionContext context) throws Exception {
140140
invokeBeforeCallbacks(BeforeContainerTemplateInvocationCallback.class, context,
141141
BeforeContainerTemplateInvocationCallback::beforeContainerTemplateInvocation);
142+
context.getThrowableCollector().assertEmpty();
142143
return context;
143144
}
144145

@@ -152,8 +153,21 @@ public JupiterEngineExecutionContext execute(JupiterEngineExecutionContext conte
152153

153154
@Override
154155
public void after(JupiterEngineExecutionContext context) throws Exception {
156+
157+
ThrowableCollector throwableCollector = context.getThrowableCollector();
158+
Throwable previousThrowable = throwableCollector.getThrowable();
159+
155160
invokeAfterCallbacks(AfterContainerTemplateInvocationCallback.class, context,
156161
AfterContainerTemplateInvocationCallback::afterContainerTemplateInvocation);
162+
163+
// If the previous Throwable was not null when this method was called,
164+
// that means an exception was already thrown either before or during
165+
// the execution of this Node. If an exception was already thrown, any
166+
// later exceptions were added as suppressed exceptions to that original
167+
// exception unless a more severe exception occurred in the meantime.
168+
if (previousThrowable != throwableCollector.getThrowable()) {
169+
throwableCollector.assertEmpty();
170+
}
157171
}
158172

159173
@Override

jupiter-tests/src/test/java/org/junit/jupiter/engine/ContainerTemplateInvocationTests.java

Lines changed: 83 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import static org.junit.jupiter.api.Assertions.assertTrue;
2020
import static org.junit.jupiter.api.Assertions.fail;
2121
import static org.junit.jupiter.api.DynamicTest.dynamicTest;
22+
import static org.junit.platform.commons.util.ExceptionUtils.throwAsUncheckedException;
2223
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
2324
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectIteration;
2425
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectMethod;
@@ -39,10 +40,12 @@
3940
import static org.junit.platform.testkit.engine.EventConditions.test;
4041
import static org.junit.platform.testkit.engine.EventConditions.uniqueId;
4142
import static org.junit.platform.testkit.engine.TestExecutionResultConditions.message;
43+
import static org.junit.platform.testkit.engine.TestExecutionResultConditions.suppressed;
4244

4345
import java.util.Collection;
4446
import java.util.List;
4547
import java.util.Map;
48+
import java.util.function.Function;
4649
import java.util.stream.IntStream;
4750
import java.util.stream.Stream;
4851

@@ -89,6 +92,9 @@
8992
import org.junit.platform.engine.UniqueId;
9093
import org.junit.platform.engine.discovery.DiscoverySelectors;
9194
import org.junit.platform.engine.reporting.ReportEntry;
95+
import org.junit.platform.testkit.engine.EngineExecutionResults;
96+
import org.opentest4j.AssertionFailedError;
97+
import org.opentest4j.TestAbortedException;
9298

9399
/**
94100
* @since 5.13
@@ -856,13 +862,8 @@ void executesLifecycleCallbacksInNestedContainerTemplates() {
856862
results.containerEvents().assertStatistics(stats -> stats.started(10).succeeded(10));
857863
results.testEvents().assertStatistics(stats -> stats.started(8).succeeded(8));
858864

859-
var callSequence = results.allEvents().reportingEntryPublished() //
860-
.map(event -> event.getRequiredPayload(ReportEntry.class)) //
861-
.map(ReportEntry::getKeyValuePairs) //
862-
.map(Map::values) //
863-
.flatMap(Collection::stream);
864865
// @formatter:off
865-
assertThat(callSequence).containsExactly(
866+
assertThat(allReportEntryValues(results)).containsExactly(
866867
"beforeAll: TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase",
867868
"beforeContainerTemplateInvocation: TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase",
868869
"beforeAll: NestedTestCase",
@@ -932,27 +933,43 @@ void guaranteesWrappingBehaviorForCallbacks() {
932933
results.containerEvents().assertStatistics(stats -> stats.started(4).succeeded(4));
933934
results.testEvents().assertStatistics(stats -> stats.started(2).succeeded(2));
934935

935-
var callSequence = results.allEvents().reportingEntryPublished() //
936-
.map(event -> event.getRequiredPayload(ReportEntry.class)) //
937-
.map(ReportEntry::getKeyValuePairs) //
938-
.map(Map::values) //
939-
.flatMap(Collection::stream);
940936
// @formatter:off
941-
assertThat(callSequence).containsExactly(
937+
assertThat(allReportEntryValues(results)).containsExactly(
942938
"1st -> beforeContainerTemplateInvocation: CallbackWrappingBehaviorTestCase",
943-
"2nd -> beforeContainerTemplateInvocation: CallbackWrappingBehaviorTestCase",
944-
"test",
945-
"2nd -> afterContainerTemplateInvocation: CallbackWrappingBehaviorTestCase",
939+
"2nd -> beforeContainerTemplateInvocation: CallbackWrappingBehaviorTestCase",
940+
"test",
941+
"2nd -> afterContainerTemplateInvocation: CallbackWrappingBehaviorTestCase",
946942
"1st -> afterContainerTemplateInvocation: CallbackWrappingBehaviorTestCase",
947943
"1st -> beforeContainerTemplateInvocation: CallbackWrappingBehaviorTestCase",
948-
"2nd -> beforeContainerTemplateInvocation: CallbackWrappingBehaviorTestCase",
949-
"test",
950-
"2nd -> afterContainerTemplateInvocation: CallbackWrappingBehaviorTestCase",
944+
"2nd -> beforeContainerTemplateInvocation: CallbackWrappingBehaviorTestCase",
945+
"test",
946+
"2nd -> afterContainerTemplateInvocation: CallbackWrappingBehaviorTestCase",
951947
"1st -> afterContainerTemplateInvocation: CallbackWrappingBehaviorTestCase"
952948
);
953949
// @formatter:on
954950
}
955951

952+
@Test
953+
void propagatesExceptionsFromCallbacks() {
954+
955+
var results = executeTestsForClass(CallbackExceptionBehaviorTestCase.class);
956+
957+
results.allEvents().assertStatistics(stats -> stats.started(4).failed(2).succeeded(2));
958+
959+
results.containerEvents().assertThatEvents() //
960+
.haveExactly(2, finishedWithFailure( //
961+
message("2nd -> afterContainerTemplateInvocation: CallbackExceptionBehaviorTestCase"), //
962+
suppressed(0,
963+
message("1st -> beforeContainerTemplateInvocation: CallbackExceptionBehaviorTestCase")), //
964+
suppressed(1,
965+
message("1st -> afterContainerTemplateInvocation: CallbackExceptionBehaviorTestCase"))));
966+
967+
assertThat(allReportEntryValues(results).distinct()) //
968+
.containsExactly("1st -> beforeContainerTemplateInvocation: CallbackExceptionBehaviorTestCase", //
969+
"2nd -> afterContainerTemplateInvocation: CallbackExceptionBehaviorTestCase", //
970+
"1st -> afterContainerTemplateInvocation: CallbackExceptionBehaviorTestCase");
971+
}
972+
956973
@Test
957974
void templateWithPreparations() {
958975
var results = executeTestsForClass(ContainerTemplateWithPreparationsTestCase.class);
@@ -963,6 +980,14 @@ void templateWithPreparations() {
963980

964981
// -------------------------------------------------------------------
965982

983+
private static Stream<String> allReportEntryValues(EngineExecutionResults results) {
984+
return results.allEvents().reportingEntryPublished() //
985+
.map(event -> event.getRequiredPayload(ReportEntry.class)) //
986+
.map(ReportEntry::getKeyValuePairs) //
987+
.map(Map::values) //
988+
.flatMap(Collection::stream);
989+
}
990+
966991
@SuppressWarnings("JUnitMalformedDeclaration")
967992
@ContainerTemplate
968993
@ExtendWith(TwoInvocationsContainerTemplateInvocationContextProvider.class)
@@ -1428,36 +1453,71 @@ static class CallbackWrappingBehaviorTestCase {
14281453
static Extension second = new ContainerTemplateInvocationCallbacks("2nd -> ");
14291454

14301455
@Test
1431-
@DisplayName("test")
14321456
void test(TestReporter testReporter) {
14331457
testReporter.publishEntry("test");
14341458
}
14351459
}
14361460

1461+
@SuppressWarnings("JUnitMalformedDeclaration")
1462+
@ExtendWith(TwoInvocationsContainerTemplateInvocationContextProvider.class)
1463+
@ContainerTemplate
1464+
static class CallbackExceptionBehaviorTestCase {
1465+
1466+
@RegisterExtension
1467+
@Order(1)
1468+
static Extension first = new ContainerTemplateInvocationCallbacks("1st -> ", TestAbortedException::new);
1469+
1470+
@RegisterExtension
1471+
@Order(2)
1472+
static Extension second = new ContainerTemplateInvocationCallbacks("2nd -> ", AssertionFailedError::new);
1473+
1474+
@Test
1475+
void test() {
1476+
fail("should not be called");
1477+
}
1478+
}
1479+
14371480
static class ContainerTemplateInvocationCallbacks
14381481
implements BeforeContainerTemplateInvocationCallback, AfterContainerTemplateInvocationCallback {
14391482

14401483
private final String prefix;
1484+
private final Function<String, Throwable> exceptionFactory;
14411485

14421486
@SuppressWarnings("unused")
14431487
ContainerTemplateInvocationCallbacks() {
14441488
this("");
14451489
}
14461490

14471491
ContainerTemplateInvocationCallbacks(String prefix) {
1492+
this(prefix, __ -> null);
1493+
}
1494+
1495+
ContainerTemplateInvocationCallbacks(String prefix, Function<String, Throwable> exceptionFactory) {
14481496
this.prefix = prefix;
1497+
this.exceptionFactory = exceptionFactory;
14491498
}
14501499

14511500
@Override
14521501
public void beforeContainerTemplateInvocation(ExtensionContext context) {
1453-
context.publishReportEntry(
1454-
prefix + "beforeContainerTemplateInvocation: " + context.getRequiredTestClass().getSimpleName());
1502+
handle("beforeContainerTemplateInvocation", context);
14551503
}
14561504

14571505
@Override
14581506
public void afterContainerTemplateInvocation(ExtensionContext context) {
1459-
context.publishReportEntry(
1460-
prefix + "afterContainerTemplateInvocation: " + context.getRequiredTestClass().getSimpleName());
1507+
handle("afterContainerTemplateInvocation", context);
1508+
}
1509+
1510+
private void handle(String methodName, ExtensionContext context) {
1511+
var message = format(methodName, context);
1512+
context.publishReportEntry(message);
1513+
var throwable = exceptionFactory.apply(message);
1514+
if (throwable != null) {
1515+
throw throwAsUncheckedException(throwable);
1516+
}
1517+
}
1518+
1519+
private String format(String methodName, ExtensionContext context) {
1520+
return "%s%s: %s".formatted(prefix, methodName, context.getRequiredTestClass().getSimpleName());
14611521
}
14621522
}
14631523

0 commit comments

Comments
 (0)