Skip to content

Commit a594ddf

Browse files
Mateusz Pietrygapietrygamat
authored andcommitted
Implement LifecycleMethodExecutionExceptionHandler
Issue #1454
1 parent dea4bb8 commit a594ddf

File tree

5 files changed

+242
-32
lines changed

5 files changed

+242
-32
lines changed
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/*
2+
* Copyright 2015-2019 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.STABLE;
14+
15+
import org.apiguardian.api.API;
16+
17+
/**
18+
* {@code LifecycleMethodExecutionExceptionHandler} defines the API for
19+
* {@link Extension Extensions} that wish to handle exceptions thrown during
20+
* execution of lifecycle methods (annotated with {@code @BeforeAll},
21+
* {@code @BeforeEach}, {@code @AfterEach} and {@code @AfterAll}.
22+
*
23+
* <p>Common use cases include swallowing an exception if it's anticipated,
24+
* logging or rolling back a transaction in certain error scenarios.
25+
*
26+
* <p>This extension needs to be declared on a class level if class level methods
27+
* ({@code @BeforeAll}, {@code @AfterAll}) are to be covered. If declared on Test
28+
* level, only handlers for {@code @BeforeEach} and {@code @AfterEach} will execute
29+
*
30+
* <h3>Constructor Requirements</h3>
31+
*
32+
* <p>Consult the documentation in {@link Extension} for details on constructor
33+
* requirements.
34+
*
35+
* @see TestExecutionExceptionHandler
36+
*
37+
* @since 5.5
38+
*/
39+
@API(status = STABLE, since = "5.5")
40+
public interface LifecycleMethodExecutionExceptionHandler extends Extension {
41+
42+
/**
43+
* Handle the supplied {@link Throwable throwable}.
44+
*
45+
* <p>Implementors must perform one of the following.
46+
* <ol>
47+
* <li>Rethrow the supplied {@code throwable} <em>as is</em> which is the default implementation</li>
48+
* <li>Swallow the supplied {@code throwable}, thereby preventing propagation.</li>
49+
* <li>Throw a new exception, potentially wrapping the supplied {@code throwable}.</li>
50+
* </ol>
51+
*
52+
* <p>If the supplied {@code throwable} is swallowed, subsequent
53+
* {@code LifecycleMethodExecutionExceptionHandler} will not be invoked;
54+
* otherwise, the next registered {@code LifecycleMethodExecutionExceptionHandler}
55+
* (if there is one) will be invoked with any {@link Throwable} thrown by
56+
* this handler.
57+
*
58+
* @param context the current extension context; never {@code null}
59+
* @param throwable the {@code Throwable} to handle; never {@code null}
60+
*/
61+
default void handleBeforeAllMethodExecutionException(ExtensionContext context, Throwable throwable)
62+
throws Throwable {
63+
throw throwable;
64+
}
65+
66+
/**
67+
* Handle the supplied {@link Throwable throwable}.
68+
*
69+
* <p>Implementors must perform one of the following.
70+
* <ol>
71+
* <li>Rethrow the supplied {@code throwable} <em>as is</em> which is the default implementation</li>
72+
* <li>Swallow the supplied {@code throwable}, thereby preventing propagation.</li>
73+
* <li>Throw a new exception, potentially wrapping the supplied {@code throwable}.</li>
74+
* </ol>
75+
*
76+
* <p>If the supplied {@code throwable} is swallowed, subsequent
77+
* {@code LifecycleMethodExecutionExceptionHandler}
78+
* will not be invoked; otherwise, the next registered
79+
* {@code LifecycleMethodExecutionExceptionHandler} (if there is one)
80+
* will be invoked with any {@link Throwable} thrown by this handler.
81+
*
82+
* @param context the current extension context; never {@code null}
83+
* @param throwable the {@code Throwable} to handle; never {@code null}
84+
*/
85+
default void handleBeforeEachMethodExecutionException(ExtensionContext context, Throwable throwable)
86+
throws Throwable {
87+
throw throwable;
88+
}
89+
90+
/**
91+
* Handle the supplied {@link Throwable throwable}.
92+
*
93+
* <p>Implementors must perform one of the following.
94+
* <ol>
95+
* <li>Rethrow the supplied {@code throwable} <em>as is</em> which is the default implementation</li>
96+
* <li>Swallow the supplied {@code throwable}, thereby preventing propagation.</li>
97+
* <li>Throw a new exception, potentially wrapping the supplied {@code throwable}.</li>
98+
* </ol>
99+
*
100+
* <p>If the supplied {@code throwable} is swallowed, subsequent
101+
* {@code LifecycleMethodExecutionExceptionHandler} will not be invoked;
102+
* otherwise, the next registered {@code LifecycleMethodExecutionExceptionHandler}
103+
* (if there is one) will be invoked with any {@link Throwable} thrown by
104+
* this handler.
105+
*
106+
* @param context the current extension context; never {@code null}
107+
* @param throwable the {@code Throwable} to handle; never {@code null}
108+
*/
109+
default void handleAfterEachMethodExecutionException(ExtensionContext context, Throwable throwable)
110+
throws Throwable {
111+
throw throwable;
112+
}
113+
114+
/**
115+
* Handle the supplied {@link Throwable throwable}.
116+
*
117+
* <p>Implementors must perform one of the following.
118+
* <ol>
119+
* <li>Rethrow the supplied {@code throwable} <em>as is</em> which is the default implementation</li>
120+
* <li>Swallow the supplied {@code throwable}, thereby preventing propagation.</li>
121+
* <li>Throw a new exception, potentially wrapping the supplied {@code throwable}.</li>
122+
* </ol>
123+
*
124+
* <p>If the supplied {@code throwable} is swallowed, subsequent
125+
* {@code LifecycleMethodExecutionExceptionHandler} will not be invoked; otherwise,
126+
* the next registered {@code LifecycleMethodExecutionExceptionHandler} (if there
127+
* is one) will be invoked with any {@link Throwable} thrown by this handler.
128+
*
129+
* @param context the current extension context; never {@code null}
130+
* @param throwable the {@code Throwable} to handle; never {@code null}
131+
*/
132+
default void handleAfterAllMethodExecutionException(ExtensionContext context, Throwable throwable)
133+
throws Throwable {
134+
throw throwable;
135+
}
136+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
* <p>Consult the documentation in {@link Extension} for details on
3131
* constructor requirements.
3232
*
33+
* @see LifecycleMethodExecutionExceptionHandler
34+
*
3335
* @since 5.0
3436
*/
3537
@FunctionalInterface

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

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import org.junit.jupiter.api.extension.ExtensionConfigurationException;
4242
import org.junit.jupiter.api.extension.ExtensionContext;
4343
import org.junit.jupiter.api.extension.InvocationInterceptor;
44+
import org.junit.jupiter.api.extension.LifecycleMethodExecutionExceptionHandler;
4445
import org.junit.jupiter.api.extension.TestInstanceFactory;
4546
import org.junit.jupiter.api.extension.TestInstancePostProcessor;
4647
import org.junit.jupiter.api.extension.TestInstances;
@@ -386,23 +387,54 @@ private void invokeBeforeAllMethods(JupiterEngineExecutionContext context) {
386387
Object testInstance = extensionContext.getTestInstance().orElse(null);
387388

388389
for (Method method : this.beforeAllMethods) {
389-
throwableCollector.execute(() -> executableInvoker.invoke(method, testInstance, extensionContext, registry,
390-
ReflectiveInterceptorCall.ofVoidMethod(InvocationInterceptor::interceptBeforeAllMethod)));
390+
throwableCollector.execute(() -> {
391+
try {
392+
executableInvoker.invoke(method, testInstance, extensionContext, registry,
393+
ReflectiveInterceptorCall.ofVoidMethod(InvocationInterceptor::interceptBeforeAllMethod));
394+
}
395+
catch (Throwable throwable) {
396+
invokeBeforeAllExecutionExceptionHandlers(registry, extensionContext, throwable);
397+
}
398+
});
391399
if (throwableCollector.isNotEmpty()) {
392400
break;
393401
}
394402
}
395403
}
396404

405+
private void invokeBeforeAllExecutionExceptionHandlers(ExtensionRegistry registry, ExtensionContext context,
406+
Throwable throwable) {
407+
408+
invokeExecutionExceptionHandlers(throwable,
409+
registry.getReversedExtensions(LifecycleMethodExecutionExceptionHandler.class),
410+
(ex, handler) -> () -> ((LifecycleMethodExecutionExceptionHandler) handler).handleBeforeAllMethodExecutionException(
411+
context, ex));
412+
}
413+
397414
private void invokeAfterAllMethods(JupiterEngineExecutionContext context) {
398415
ExtensionRegistry registry = context.getExtensionRegistry();
399416
ExtensionContext extensionContext = context.getExtensionContext();
400417
ThrowableCollector throwableCollector = context.getThrowableCollector();
401418
Object testInstance = extensionContext.getTestInstance().orElse(null);
402419

403-
this.afterAllMethods.forEach(
404-
method -> throwableCollector.execute(() -> executableInvoker.invoke(method, testInstance, extensionContext,
405-
registry, ReflectiveInterceptorCall.ofVoidMethod(InvocationInterceptor::interceptAfterAllMethod))));
420+
this.afterAllMethods.forEach(method -> throwableCollector.execute(() -> {
421+
try {
422+
executableInvoker.invoke(method, testInstance, extensionContext, registry,
423+
ReflectiveInterceptorCall.ofVoidMethod(InvocationInterceptor::interceptAfterAllMethod));
424+
}
425+
catch (Throwable throwable) {
426+
invokeAfterAllExecutionExceptionHandlers(registry, extensionContext, throwable);
427+
}
428+
}));
429+
}
430+
431+
private void invokeAfterAllExecutionExceptionHandlers(ExtensionRegistry registry, ExtensionContext context,
432+
Throwable throwable) {
433+
434+
invokeExecutionExceptionHandlers(throwable,
435+
registry.getReversedExtensions(LifecycleMethodExecutionExceptionHandler.class),
436+
(ex, handler) -> () -> ((LifecycleMethodExecutionExceptionHandler) handler).handleAfterAllMethodExecutionException(
437+
context, ex));
406438
}
407439

408440
private void invokeAfterAllCallbacks(JupiterEngineExecutionContext context) {

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,16 @@
2121
import java.lang.reflect.AnnotatedElement;
2222
import java.util.Collections;
2323
import java.util.LinkedHashSet;
24+
import java.util.List;
2425
import java.util.Optional;
2526
import java.util.Set;
27+
import java.util.function.BiFunction;
2628
import java.util.function.Supplier;
2729

2830
import org.apiguardian.api.API;
2931
import org.junit.jupiter.api.Tag;
3032
import org.junit.jupiter.api.extension.ConditionEvaluationResult;
33+
import org.junit.jupiter.api.extension.Extension;
3134
import org.junit.jupiter.api.parallel.Execution;
3235
import org.junit.jupiter.api.parallel.ResourceAccessMode;
3336
import org.junit.jupiter.api.parallel.ResourceLock;
@@ -37,6 +40,8 @@
3740
import org.junit.platform.commons.JUnitException;
3841
import org.junit.platform.commons.logging.Logger;
3942
import org.junit.platform.commons.logging.LoggerFactory;
43+
import org.junit.platform.commons.util.BlacklistedExceptions;
44+
import org.junit.platform.commons.util.ExceptionUtils;
4045
import org.junit.platform.engine.TestDescriptor;
4146
import org.junit.platform.engine.TestSource;
4247
import org.junit.platform.engine.TestTag;
@@ -45,6 +50,7 @@
4550
import org.junit.platform.engine.support.hierarchical.ExclusiveResource;
4651
import org.junit.platform.engine.support.hierarchical.ExclusiveResource.LockMode;
4752
import org.junit.platform.engine.support.hierarchical.Node;
53+
import org.junit.platform.engine.support.hierarchical.ThrowableCollector;
4854

4955
/**
5056
* @since 5.0
@@ -95,6 +101,28 @@ protected static Set<TestTag> getTags(AnnotatedElement element) {
95101
// @formatter:on
96102
}
97103

104+
/**
105+
* Invokes handlers on the {@code Throwable} one by one until none are left or the throwable to handle
106+
* has been swallowed.
107+
*/
108+
void invokeExecutionExceptionHandlers(Throwable throwable, List<? extends Extension> handlers,
109+
BiFunction<Throwable, Extension, ThrowableCollector.Executable> generator) {
110+
// No handlers left?
111+
if (handlers.isEmpty()) {
112+
ExceptionUtils.throwAsUncheckedException(throwable);
113+
}
114+
115+
try {
116+
// Invoke next available handler
117+
ThrowableCollector.Executable executable = generator.apply(throwable, handlers.remove(0));
118+
executable.execute();
119+
}
120+
catch (Throwable handledThrowable) {
121+
BlacklistedExceptions.rethrowIfBlacklisted(handledThrowable);
122+
invokeExecutionExceptionHandlers(handledThrowable, handlers, generator);
123+
}
124+
}
125+
98126
// --- Node ----------------------------------------------------------------
99127

100128
@Override

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

Lines changed: 39 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.junit.jupiter.api.extension.Extension;
2929
import org.junit.jupiter.api.extension.ExtensionContext;
3030
import org.junit.jupiter.api.extension.InvocationInterceptor;
31+
import org.junit.jupiter.api.extension.LifecycleMethodExecutionExceptionHandler;
3132
import org.junit.jupiter.api.extension.TestExecutionExceptionHandler;
3233
import org.junit.jupiter.api.extension.TestInstances;
3334
import org.junit.jupiter.api.extension.TestWatcher;
@@ -41,7 +42,6 @@
4142
import org.junit.platform.commons.logging.Logger;
4243
import org.junit.platform.commons.logging.LoggerFactory;
4344
import org.junit.platform.commons.util.BlacklistedExceptions;
44-
import org.junit.platform.commons.util.ExceptionUtils;
4545
import org.junit.platform.commons.util.ReflectionUtils;
4646
import org.junit.platform.engine.TestDescriptor;
4747
import org.junit.platform.engine.TestExecutionResult;
@@ -154,9 +154,23 @@ private void invokeBeforeEachCallbacks(JupiterEngineExecutionContext context) {
154154

155155
private void invokeBeforeEachMethods(JupiterEngineExecutionContext context) {
156156
ExtensionRegistry registry = context.getExtensionRegistry();
157-
invokeBeforeMethodsOrCallbacksUntilExceptionOccurs(context,
158-
((extensionContext, adapter) -> () -> adapter.invokeBeforeEachMethod(extensionContext, registry)),
159-
BeforeEachMethodAdapter.class);
157+
invokeBeforeMethodsOrCallbacksUntilExceptionOccurs(context, ((extensionContext, adapter) -> () -> {
158+
try {
159+
adapter.invokeBeforeEachMethod(extensionContext, registry);
160+
}
161+
catch (Throwable throwable) {
162+
invokeBeforeEachExecutionExceptionHandlers(extensionContext, registry, throwable);
163+
}
164+
}), BeforeEachMethodAdapter.class);
165+
}
166+
167+
private void invokeBeforeEachExecutionExceptionHandlers(ExtensionContext context, ExtensionRegistry registry,
168+
Throwable throwable) {
169+
170+
invokeExecutionExceptionHandlers(throwable,
171+
registry.getReversedExtensions(LifecycleMethodExecutionExceptionHandler.class),
172+
(ex, handler) -> () -> ((LifecycleMethodExecutionExceptionHandler) handler).handleBeforeEachMethodExecutionException(
173+
context, ex));
160174
}
161175

162176
private void invokeBeforeTestExecutionCallbacks(JupiterEngineExecutionContext context) {
@@ -203,26 +217,9 @@ protected void invokeTestMethod(JupiterEngineExecutionContext context, DynamicTe
203217
private void invokeTestExecutionExceptionHandlers(ExtensionRegistry registry, ExtensionContext context,
204218
Throwable ex) {
205219

206-
invokeTestExecutionExceptionHandlers(ex, registry.getReversedExtensions(TestExecutionExceptionHandler.class),
207-
context);
208-
}
209-
210-
private void invokeTestExecutionExceptionHandlers(Throwable ex, List<TestExecutionExceptionHandler> handlers,
211-
ExtensionContext context) {
212-
213-
// No handlers left?
214-
if (handlers.isEmpty()) {
215-
ExceptionUtils.throwAsUncheckedException(ex);
216-
}
217-
218-
try {
219-
// Invoke next available handler
220-
handlers.remove(0).handleTestExecutionException(context, ex);
221-
}
222-
catch (Throwable t) {
223-
BlacklistedExceptions.rethrowIfBlacklisted(t);
224-
invokeTestExecutionExceptionHandlers(t, handlers, context);
225-
}
220+
invokeExecutionExceptionHandlers(ex, registry.getReversedExtensions(TestExecutionExceptionHandler.class),
221+
(throwable, handler) -> () -> ((TestExecutionExceptionHandler) handler).handleTestExecutionException(
222+
context, throwable));
226223
}
227224

228225
private void invokeAfterTestExecutionCallbacks(JupiterEngineExecutionContext context) {
@@ -233,9 +230,24 @@ private void invokeAfterTestExecutionCallbacks(JupiterEngineExecutionContext con
233230

234231
private void invokeAfterEachMethods(JupiterEngineExecutionContext context) {
235232
ExtensionRegistry registry = context.getExtensionRegistry();
236-
invokeAllAfterMethodsOrCallbacks(context,
237-
((extensionContext, adapter) -> () -> adapter.invokeAfterEachMethod(extensionContext, registry)),
238-
AfterEachMethodAdapter.class);
233+
invokeAllAfterMethodsOrCallbacks(context, ((extensionContext, adapter) -> () -> {
234+
try {
235+
adapter.invokeAfterEachMethod(extensionContext, registry);
236+
}
237+
catch (Throwable throwable) {
238+
invokeAfterEachExecutionExceptionHandlers(extensionContext, registry, throwable);
239+
}
240+
}), AfterEachMethodAdapter.class);
241+
}
242+
243+
private void invokeAfterEachExecutionExceptionHandlers(ExtensionContext context, ExtensionRegistry registry,
244+
Throwable throwable) {
245+
246+
invokeExecutionExceptionHandlers(throwable,
247+
registry.getReversedExtensions(LifecycleMethodExecutionExceptionHandler.class),
248+
(ex, handler) -> () -> ((LifecycleMethodExecutionExceptionHandler) handler).handleAfterEachMethodExecutionException(
249+
context, ex));
250+
239251
}
240252

241253
private void invokeAfterEachCallbacks(JupiterEngineExecutionContext context) {

0 commit comments

Comments
 (0)