Skip to content

Commit

Permalink
Propagate errors from callbacks
Browse files Browse the repository at this point in the history
  • Loading branch information
marcphilipp committed Mar 1, 2025
1 parent bc03ecb commit 47291a3
Show file tree
Hide file tree
Showing 2 changed files with 97 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ public SkipResult shouldBeSkipped(JupiterEngineExecutionContext context) {
public JupiterEngineExecutionContext before(JupiterEngineExecutionContext context) throws Exception {
invokeBeforeCallbacks(BeforeContainerTemplateInvocationCallback.class, context,
BeforeContainerTemplateInvocationCallback::beforeContainerTemplateInvocation);
context.getThrowableCollector().assertEmpty();
return context;
}

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

@Override
public void after(JupiterEngineExecutionContext context) throws Exception {

ThrowableCollector throwableCollector = context.getThrowableCollector();
Throwable previousThrowable = throwableCollector.getThrowable();

invokeAfterCallbacks(AfterContainerTemplateInvocationCallback.class, context,
AfterContainerTemplateInvocationCallback::afterContainerTemplateInvocation);

// If the previous Throwable was not null when this method was called,
// that means an exception was already thrown either before or during
// the execution of this Node. If an exception was already thrown, any
// later exceptions were added as suppressed exceptions to that original
// exception unless a more severe exception occurred in the meantime.
if (previousThrowable != throwableCollector.getThrowable()) {
throwableCollector.assertEmpty();
}
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;
import static org.junit.platform.commons.util.ExceptionUtils.throwAsUncheckedException;
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectIteration;
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectMethod;
Expand All @@ -39,10 +40,12 @@
import static org.junit.platform.testkit.engine.EventConditions.test;
import static org.junit.platform.testkit.engine.EventConditions.uniqueId;
import static org.junit.platform.testkit.engine.TestExecutionResultConditions.message;
import static org.junit.platform.testkit.engine.TestExecutionResultConditions.suppressed;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.IntStream;
import java.util.stream.Stream;

Expand Down Expand Up @@ -89,6 +92,9 @@
import org.junit.platform.engine.UniqueId;
import org.junit.platform.engine.discovery.DiscoverySelectors;
import org.junit.platform.engine.reporting.ReportEntry;
import org.junit.platform.testkit.engine.EngineExecutionResults;
import org.opentest4j.AssertionFailedError;
import org.opentest4j.TestAbortedException;

/**
* @since 5.13
Expand Down Expand Up @@ -856,13 +862,8 @@ void executesLifecycleCallbacksInNestedContainerTemplates() {
results.containerEvents().assertStatistics(stats -> stats.started(10).succeeded(10));
results.testEvents().assertStatistics(stats -> stats.started(8).succeeded(8));

var callSequence = results.allEvents().reportingEntryPublished() //
.map(event -> event.getRequiredPayload(ReportEntry.class)) //
.map(ReportEntry::getKeyValuePairs) //
.map(Map::values) //
.flatMap(Collection::stream);
// @formatter:off
assertThat(callSequence).containsExactly(
assertThat(allReportEntryValues(results)).containsExactly(
"beforeAll: TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase",
"beforeContainerTemplateInvocation: TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase",
"beforeAll: NestedTestCase",
Expand Down Expand Up @@ -932,27 +933,43 @@ void guaranteesWrappingBehaviorForCallbacks() {
results.containerEvents().assertStatistics(stats -> stats.started(4).succeeded(4));
results.testEvents().assertStatistics(stats -> stats.started(2).succeeded(2));

var callSequence = results.allEvents().reportingEntryPublished() //
.map(event -> event.getRequiredPayload(ReportEntry.class)) //
.map(ReportEntry::getKeyValuePairs) //
.map(Map::values) //
.flatMap(Collection::stream);
// @formatter:off
assertThat(callSequence).containsExactly(
assertThat(allReportEntryValues(results)).containsExactly(
"1st -> beforeContainerTemplateInvocation: CallbackWrappingBehaviorTestCase",
"2nd -> beforeContainerTemplateInvocation: CallbackWrappingBehaviorTestCase",
"test",
"2nd -> afterContainerTemplateInvocation: CallbackWrappingBehaviorTestCase",
"2nd -> beforeContainerTemplateInvocation: CallbackWrappingBehaviorTestCase",
"test",
"2nd -> afterContainerTemplateInvocation: CallbackWrappingBehaviorTestCase",
"1st -> afterContainerTemplateInvocation: CallbackWrappingBehaviorTestCase",
"1st -> beforeContainerTemplateInvocation: CallbackWrappingBehaviorTestCase",
"2nd -> beforeContainerTemplateInvocation: CallbackWrappingBehaviorTestCase",
"test",
"2nd -> afterContainerTemplateInvocation: CallbackWrappingBehaviorTestCase",
"2nd -> beforeContainerTemplateInvocation: CallbackWrappingBehaviorTestCase",
"test",
"2nd -> afterContainerTemplateInvocation: CallbackWrappingBehaviorTestCase",
"1st -> afterContainerTemplateInvocation: CallbackWrappingBehaviorTestCase"
);
// @formatter:on
}

@Test
void propagatesExceptionsFromCallbacks() {

var results = executeTestsForClass(CallbackExceptionBehaviorTestCase.class);

results.allEvents().assertStatistics(stats -> stats.started(4).failed(2).succeeded(2));

results.containerEvents().assertThatEvents() //
.haveExactly(2, finishedWithFailure( //
message("2nd -> afterContainerTemplateInvocation: CallbackExceptionBehaviorTestCase"), //
suppressed(0,
message("1st -> beforeContainerTemplateInvocation: CallbackExceptionBehaviorTestCase")), //
suppressed(1,
message("1st -> afterContainerTemplateInvocation: CallbackExceptionBehaviorTestCase"))));

assertThat(allReportEntryValues(results).distinct()) //
.containsExactly("1st -> beforeContainerTemplateInvocation: CallbackExceptionBehaviorTestCase", //
"2nd -> afterContainerTemplateInvocation: CallbackExceptionBehaviorTestCase", //
"1st -> afterContainerTemplateInvocation: CallbackExceptionBehaviorTestCase");
}

@Test
void templateWithPreparations() {
var results = executeTestsForClass(ContainerTemplateWithPreparationsTestCase.class);
Expand All @@ -963,6 +980,14 @@ void templateWithPreparations() {

// -------------------------------------------------------------------

private static Stream<String> allReportEntryValues(EngineExecutionResults results) {
return results.allEvents().reportingEntryPublished() //
.map(event -> event.getRequiredPayload(ReportEntry.class)) //
.map(ReportEntry::getKeyValuePairs) //
.map(Map::values) //
.flatMap(Collection::stream);
}

@SuppressWarnings("JUnitMalformedDeclaration")
@ContainerTemplate
@ExtendWith(TwoInvocationsContainerTemplateInvocationContextProvider.class)
Expand Down Expand Up @@ -1428,36 +1453,71 @@ static class CallbackWrappingBehaviorTestCase {
static Extension second = new ContainerTemplateInvocationCallbacks("2nd -> ");

@Test
@DisplayName("test")
void test(TestReporter testReporter) {
testReporter.publishEntry("test");
}
}

@SuppressWarnings("JUnitMalformedDeclaration")
@ExtendWith(TwoInvocationsContainerTemplateInvocationContextProvider.class)
@ContainerTemplate
static class CallbackExceptionBehaviorTestCase {

@RegisterExtension
@Order(1)
static Extension first = new ContainerTemplateInvocationCallbacks("1st -> ", TestAbortedException::new);

@RegisterExtension
@Order(2)
static Extension second = new ContainerTemplateInvocationCallbacks("2nd -> ", AssertionFailedError::new);

@Test
void test() {
fail("should not be called");
}
}

static class ContainerTemplateInvocationCallbacks
implements BeforeContainerTemplateInvocationCallback, AfterContainerTemplateInvocationCallback {

private final String prefix;
private final Function<String, Throwable> exceptionFactory;

@SuppressWarnings("unused")
ContainerTemplateInvocationCallbacks() {
this("");
}

ContainerTemplateInvocationCallbacks(String prefix) {
this(prefix, __ -> null);
}

ContainerTemplateInvocationCallbacks(String prefix, Function<String, Throwable> exceptionFactory) {
this.prefix = prefix;
this.exceptionFactory = exceptionFactory;
}

@Override
public void beforeContainerTemplateInvocation(ExtensionContext context) {
context.publishReportEntry(
prefix + "beforeContainerTemplateInvocation: " + context.getRequiredTestClass().getSimpleName());
handle("beforeContainerTemplateInvocation", context);
}

@Override
public void afterContainerTemplateInvocation(ExtensionContext context) {
context.publishReportEntry(
prefix + "afterContainerTemplateInvocation: " + context.getRequiredTestClass().getSimpleName());
handle("afterContainerTemplateInvocation", context);
}

private void handle(String methodName, ExtensionContext context) {
var message = format(methodName, context);
context.publishReportEntry(message);
var throwable = exceptionFactory.apply(message);
if (throwable != null) {
throw throwAsUncheckedException(throwable);
}
}

private String format(String methodName, ExtensionContext context) {
return "%s%s: %s".formatted(prefix, methodName, context.getRequiredTestClass().getSimpleName());
}
}

Expand Down

0 comments on commit 47291a3

Please sign in to comment.