diff --git a/documentation/src/docs/asciidoc/link-attributes.adoc b/documentation/src/docs/asciidoc/link-attributes.adoc index de75ec74cb36..841c6f3cbcd2 100644 --- a/documentation/src/docs/asciidoc/link-attributes.adoc +++ b/documentation/src/docs/asciidoc/link-attributes.adoc @@ -81,6 +81,7 @@ endif::[] :ExtensionContext: {javadoc-root}/org/junit/jupiter/api/extension/ExtensionContext.html[ExtensionContext] :ExtensionContext_Store: {javadoc-root}/org/junit/jupiter/api/extension/ExtensionContext.Store.html[Store] :InvocationInterceptor: {javadoc-root}/org/junit/jupiter/api/extension/InvocationInterceptor.html[InvocationInterceptor] +:LifecycleMethodExecutionExceptionHandler: {javadoc-root}/org/junit/jupiter/api/extension/LifecycleMethodExecutionExceptionHandler.html[LifecycleMethodExecutionExceptionHandler] :ParameterResolver: {javadoc-root}/org/junit/jupiter/api/extension/ParameterResolver.html[ParameterResolver] :RegisterExtension: {javadoc-root}/org/junit/jupiter/api/extension/RegisterExtension.html[@RegisterExtension] :TestExecutionExceptionHandler: {javadoc-root}/org/junit/jupiter/api/extension/TestExecutionExceptionHandler.html[TestExecutionExceptionHandler] diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.5.0-RC1.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.5.0-RC1.adoc index b6e19fd7fdd3..f9c3c0d55e3d 100644 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.5.0-RC1.adoc +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.5.0-RC1.adoc @@ -79,6 +79,10 @@ on GitHub. * New `InvocationInterceptor` extension API (see <<../user-guide/index.adoc#extensions-intercepting-invocations, User Guide>> for details). +* New extension APIs for handling exceptions during execution of lifecycle methods: + `@BeforeAll`, `@BeforeEach`, `@AfterEach` and `@AfterAll` (see + <<../user-guide/index.adoc#extensions-exception-handling, User Guide>> for + details) * Added support for method URIs, e.g. `method:org.junit.Foo#bar()`, to `DynamicContainer` and `DynamicTest` factory methods. diff --git a/documentation/src/docs/asciidoc/user-guide/extensions.adoc b/documentation/src/docs/asciidoc/user-guide/extensions.adoc index 3b6160c01766..942553ace859 100644 --- a/documentation/src/docs/asciidoc/user-guide/extensions.adoc +++ b/documentation/src/docs/asciidoc/user-guide/extensions.adoc @@ -433,18 +433,51 @@ INFO: Method [sleep50ms] took 53 ms. [[extensions-exception-handling]] === Exception Handling -`{TestExecutionExceptionHandler}` defines the API for `Extensions` that wish to handle -exceptions thrown during test execution. +Exceptions thrown during the test execution may be intercepted and handled accordingly +before propagating further, so that certain actions like error logging or resource releasing +may be defined in specialized `Extensions`. JUnit Jupiter offers API for `Extensions` that +wish to handle exceptions thrown during `@Test` methods via `{TestExecutionExceptionHandler}` +and for those thrown during one of test lifecycle methods (`@BeforeAll`, `@BeforeEach`, +`@AfterEach` and `@AfterAll`) via `{LifecycleMethodExecutionExceptionHandler}`. The following example shows an extension which will swallow all instances of `IOException` but rethrow any other type of exception. [source,java,indent=0] -.An exception handling extension +.An exception handling extension that filters IOExceptions in test execution ---- include::{testDir}/example/exception/IgnoreIOExceptionExtension.java[tags=user_guide] ---- +Another example shows how to record the state of an application under test exactly at +the point of unexpected exception being thrown during setup and cleanup. Note that unlike +relying on lifecycle callbacks, which may or may not be executed depending on the test +status, this solution guarantees execution immediately after failing `@BeforeAll`, +`@BeforeEach`, `@AfterEach` or `@AfterAll`. + +[source,java,indent=0] +.An exception handling extension that records application state on error +---- +include::{testDir}/example/exception/RecordStateOnErrorExtension.java[tags=user_guide] +---- + +Multiple execution exception handlers may be invoked for the same lifecycle method in +order of declaration. If one of the handlers swallows the handled exception, subsequent +ones will not be executed, and no failure will be propagated to JUnit engine, as if the +exception was never thrown. Handlers may also choose to rethrow the exception or throw +a different one, potentially wrapping the original. + +Extensions implementing `{LifecycleMethodExecutionExceptionHandler}` that wish to handle +exceptions thrown during `@BeforeAll` or `@AfterAll` need to be registered on a class level, +while handlers for `BeforeEach` and `AfterEach` may be also registered for individual +test methods. + +[source,java,indent=0] +.Registering multiple exception handling extensions +---- +include::{testDir}/example/exception/MultipleHandlersTestCase.java[tags=user_guide] +---- + [[extensions-intercepting-invocations]] === Intercepting Invocations @@ -584,7 +617,7 @@ test method and will be repeated for every test method in the test class. [#extensions-execution-order-diagram,reftext='{figure-caption}'] image::extensions_lifecycle.png[caption='',title='{figure-caption}'] -The following table further explains the twelve steps in the +The following table further explains the sixteen steps in the <> diagram. [cols="5,15,80"] @@ -600,48 +633,68 @@ The following table further explains the twelve steps in the | user code executed before all tests of the container are executed | 3 +| interface `org.junit.jupiter.api.extension.LifecycleMethodExecutionExceptionHandler +#handleBeforeAllMethodExecutionException` +| extension code for handling exceptions thrown during a method annotated with `@BeforeAll` + +| 4 | interface `org.junit.jupiter.api.extension.BeforeEachCallback` | extension code executed before each test is executed -| 4 +| 5 | annotation `org.junit.jupiter.api.BeforeEach` | user code executed before each test is executed -| 5 +| 6 +| interface `org.junit.jupiter.api.extension.LifecycleMethodExecutionExceptionHandler +#handleBeforeEachMethodExecutionException` +| extension code for handling exceptions thrown during a method annotated with `@BeforeEach` + +| 7 | interface `org.junit.jupiter.api.extension.BeforeTestExecutionCallback` | extension code executed immediately before a test is executed -| 6 +| 8 | annotation `org.junit.jupiter.api.Test` | user code of the actual test method -| 7 +| 9 | interface `org.junit.jupiter.api.extension.TestExecutionExceptionHandler` | extension code for handling exceptions thrown during a test -| 8 +| 10 | interface `org.junit.jupiter.api.extension.AfterTestExecutionCallback` | extension code executed immediately after test execution and its corresponding exception handlers -| 9 +| 11 | annotation `org.junit.jupiter.api.AfterEach` | user code executed after each test is executed -| 10 +| 12 +| interface `org.junit.jupiter.api.extension.LifecycleMethodExecutionExceptionHandler +#handleAfterEachMethodExecutionException` +| extension code for handling exceptions thrown during a method annotated with `@AfterEach` + +| 13 | interface `org.junit.jupiter.api.extension.AfterEachCallback` | extension code executed after each test is executed -| 11 +| 14 | annotation `org.junit.jupiter.api.AfterAll` | user code executed after all tests of the container are executed -| 12 +| 15 +| interface `org.junit.jupiter.api.extension.LifecycleMethodExecutionExceptionHandler +#handleAfterAllMethodExecutionException` +| extension code for handling exceptions thrown during a method annotated with `@AfterAll` + +| 16 | interface `org.junit.jupiter.api.extension.AfterAllCallback` | extension code executed after all tests of the container are executed |=== -In the simplest case only the actual test method will be executed (step 6); all other +In the simplest case only the actual test method will be executed (step 8); all other steps are optional depending on the presence of user code or extension support for the corresponding lifecycle callback. For further details on the various lifecycle callbacks please consult the respective Javadoc for each annotation and extension. diff --git a/documentation/src/docs/asciidoc/user-guide/images/extensions_lifecycle.png b/documentation/src/docs/asciidoc/user-guide/images/extensions_lifecycle.png index c55713437b33..7f867b8a7d1f 100644 Binary files a/documentation/src/docs/asciidoc/user-guide/images/extensions_lifecycle.png and b/documentation/src/docs/asciidoc/user-guide/images/extensions_lifecycle.png differ diff --git a/documentation/src/test/java/example/exception/MultipleHandlersTestCase.java b/documentation/src/test/java/example/exception/MultipleHandlersTestCase.java new file mode 100644 index 000000000000..9dc53fd5ae8c --- /dev/null +++ b/documentation/src/test/java/example/exception/MultipleHandlersTestCase.java @@ -0,0 +1,62 @@ +/* + * Copyright 2015-2019 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package example.exception; + +import static example.exception.MultipleHandlersTestCase.ThirdExecutedHandler; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.LifecycleMethodExecutionExceptionHandler; +import org.junit.jupiter.api.extension.TestExecutionExceptionHandler; + +// @formatter:off +// tag::user_guide[] +// Register handlers for @Test, @BeforeEach, @AfterEach as well as @BeforeAll and @AfterAll +@ExtendWith(ThirdExecutedHandler.class) +class MultipleHandlersTestCase { + + // Register handlers for @Test, @BeforeEach, @AfterEach only + @ExtendWith(SecondExecutedHandler.class) + @ExtendWith(FirstExecutedHandler.class) + @Test + void testMethod() { + } + + // end::user_guide[] + + static class FirstExecutedHandler implements TestExecutionExceptionHandler { + @Override + public void handleTestExecutionException(ExtensionContext context, Throwable ex) + throws Throwable { + throw ex; + } + } + + static class SecondExecutedHandler implements LifecycleMethodExecutionExceptionHandler { + @Override + public void handleBeforeEachMethodExecutionException(ExtensionContext context, Throwable ex) + throws Throwable { + throw ex; + } + } + + static class ThirdExecutedHandler implements LifecycleMethodExecutionExceptionHandler { + @Override + public void handleBeforeAllMethodExecutionException(ExtensionContext context, Throwable ex) + throws Throwable { + throw ex; + } + } + // tag::user_guide[] +} +// end::user_guide[] +// @formatter:on diff --git a/documentation/src/test/java/example/exception/RecordStateOnErrorExtension.java b/documentation/src/test/java/example/exception/RecordStateOnErrorExtension.java new file mode 100644 index 000000000000..1e4cc7bf0082 --- /dev/null +++ b/documentation/src/test/java/example/exception/RecordStateOnErrorExtension.java @@ -0,0 +1,55 @@ +/* + * Copyright 2015-2019 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package example.exception; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.LifecycleMethodExecutionExceptionHandler; + +// @formatter:off +// tag::user_guide[] +class RecordStateOnErrorExtension implements LifecycleMethodExecutionExceptionHandler { + + @Override + public void handleBeforeAllMethodExecutionException(ExtensionContext context, Throwable ex) + throws Throwable { + memoryDumpForFurtherInvestigation("Failure recorded during class setup"); + throw ex; + } + + @Override + public void handleBeforeEachMethodExecutionException(ExtensionContext context, Throwable ex) + throws Throwable { + memoryDumpForFurtherInvestigation("Failure recorded during test setup"); + throw ex; + } + + @Override + public void handleAfterEachMethodExecutionException(ExtensionContext context, Throwable ex) + throws Throwable { + memoryDumpForFurtherInvestigation("Failure recorded during test cleanup"); + throw ex; + } + + @Override + public void handleAfterAllMethodExecutionException(ExtensionContext context, Throwable ex) + throws Throwable { + memoryDumpForFurtherInvestigation("Failure recorded during class cleanup"); + throw ex; + } + // end::user_guide[] + + private void memoryDumpForFurtherInvestigation(String error) { + + } + // tag::user_guide[] +} +// end::user_guide[] +// @formatter:on diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/LifecycleMethodExecutionExceptionHandler.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/LifecycleMethodExecutionExceptionHandler.java new file mode 100644 index 000000000000..b428eebfcfda --- /dev/null +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/LifecycleMethodExecutionExceptionHandler.java @@ -0,0 +1,136 @@ +/* + * Copyright 2015-2019 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.jupiter.api.extension; + +import static org.apiguardian.api.API.Status.STABLE; + +import org.apiguardian.api.API; + +/** + * {@code LifecycleMethodExecutionExceptionHandler} defines the API for + * {@link Extension Extensions} that wish to handle exceptions thrown during + * execution of lifecycle methods (annotated with {@code @BeforeAll}, + * {@code @BeforeEach}, {@code @AfterEach} and {@code @AfterAll}. + * + *

Common use cases include swallowing an exception if it's anticipated, + * logging or rolling back a transaction in certain error scenarios. + * + *

This extension needs to be declared on a class level if class level methods + * ({@code @BeforeAll}, {@code @AfterAll}) are to be covered. If declared on Test + * level, only handlers for {@code @BeforeEach} and {@code @AfterEach} will execute + * + *

Constructor Requirements

+ * + *

Consult the documentation in {@link Extension} for details on constructor + * requirements. + * + * @see TestExecutionExceptionHandler + * + * @since 5.5 + */ +@API(status = STABLE, since = "5.5") +public interface LifecycleMethodExecutionExceptionHandler extends Extension { + + /** + * Handle the supplied {@link Throwable throwable}. + * + *

Implementors must perform one of the following. + *

    + *
  1. Rethrow the supplied {@code throwable} as is which is the default implementation
  2. + *
  3. Swallow the supplied {@code throwable}, thereby preventing propagation.
  4. + *
  5. Throw a new exception, potentially wrapping the supplied {@code throwable}.
  6. + *
+ * + *

If the supplied {@code throwable} is swallowed, subsequent + * {@code LifecycleMethodExecutionExceptionHandler} will not be invoked; + * otherwise, the next registered {@code LifecycleMethodExecutionExceptionHandler} + * (if there is one) will be invoked with any {@link Throwable} thrown by + * this handler. + * + * @param context the current extension context; never {@code null} + * @param throwable the {@code Throwable} to handle; never {@code null} + */ + default void handleBeforeAllMethodExecutionException(ExtensionContext context, Throwable throwable) + throws Throwable { + throw throwable; + } + + /** + * Handle the supplied {@link Throwable throwable}. + * + *

Implementors must perform one of the following. + *

    + *
  1. Rethrow the supplied {@code throwable} as is which is the default implementation
  2. + *
  3. Swallow the supplied {@code throwable}, thereby preventing propagation.
  4. + *
  5. Throw a new exception, potentially wrapping the supplied {@code throwable}.
  6. + *
+ * + *

If the supplied {@code throwable} is swallowed, subsequent + * {@code LifecycleMethodExecutionExceptionHandler} + * will not be invoked; otherwise, the next registered + * {@code LifecycleMethodExecutionExceptionHandler} (if there is one) + * will be invoked with any {@link Throwable} thrown by this handler. + * + * @param context the current extension context; never {@code null} + * @param throwable the {@code Throwable} to handle; never {@code null} + */ + default void handleBeforeEachMethodExecutionException(ExtensionContext context, Throwable throwable) + throws Throwable { + throw throwable; + } + + /** + * Handle the supplied {@link Throwable throwable}. + * + *

Implementors must perform one of the following. + *

    + *
  1. Rethrow the supplied {@code throwable} as is which is the default implementation
  2. + *
  3. Swallow the supplied {@code throwable}, thereby preventing propagation.
  4. + *
  5. Throw a new exception, potentially wrapping the supplied {@code throwable}.
  6. + *
+ * + *

If the supplied {@code throwable} is swallowed, subsequent + * {@code LifecycleMethodExecutionExceptionHandler} will not be invoked; + * otherwise, the next registered {@code LifecycleMethodExecutionExceptionHandler} + * (if there is one) will be invoked with any {@link Throwable} thrown by + * this handler. + * + * @param context the current extension context; never {@code null} + * @param throwable the {@code Throwable} to handle; never {@code null} + */ + default void handleAfterEachMethodExecutionException(ExtensionContext context, Throwable throwable) + throws Throwable { + throw throwable; + } + + /** + * Handle the supplied {@link Throwable throwable}. + * + *

Implementors must perform one of the following. + *

    + *
  1. Rethrow the supplied {@code throwable} as is which is the default implementation
  2. + *
  3. Swallow the supplied {@code throwable}, thereby preventing propagation.
  4. + *
  5. Throw a new exception, potentially wrapping the supplied {@code throwable}.
  6. + *
+ * + *

If the supplied {@code throwable} is swallowed, subsequent + * {@code LifecycleMethodExecutionExceptionHandler} will not be invoked; otherwise, + * the next registered {@code LifecycleMethodExecutionExceptionHandler} (if there + * is one) will be invoked with any {@link Throwable} thrown by this handler. + * + * @param context the current extension context; never {@code null} + * @param throwable the {@code Throwable} to handle; never {@code null} + */ + default void handleAfterAllMethodExecutionException(ExtensionContext context, Throwable throwable) + throws Throwable { + throw throwable; + } +} diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/TestExecutionExceptionHandler.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/TestExecutionExceptionHandler.java index 852261871e84..dcf88b004445 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/TestExecutionExceptionHandler.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/TestExecutionExceptionHandler.java @@ -30,6 +30,8 @@ *

Consult the documentation in {@link Extension} for details on * constructor requirements. * + * @see LifecycleMethodExecutionExceptionHandler + * * @since 5.0 */ @FunctionalInterface diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassTestDescriptor.java index bd1f7c320523..97a40e2f689e 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassTestDescriptor.java @@ -41,6 +41,7 @@ import org.junit.jupiter.api.extension.ExtensionConfigurationException; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.InvocationInterceptor; +import org.junit.jupiter.api.extension.LifecycleMethodExecutionExceptionHandler; import org.junit.jupiter.api.extension.TestInstanceFactory; import org.junit.jupiter.api.extension.TestInstancePostProcessor; import org.junit.jupiter.api.extension.TestInstances; @@ -386,23 +387,54 @@ private void invokeBeforeAllMethods(JupiterEngineExecutionContext context) { Object testInstance = extensionContext.getTestInstance().orElse(null); for (Method method : this.beforeAllMethods) { - throwableCollector.execute(() -> executableInvoker.invoke(method, testInstance, extensionContext, registry, - ReflectiveInterceptorCall.ofVoidMethod(InvocationInterceptor::interceptBeforeAllMethod))); + throwableCollector.execute(() -> { + try { + executableInvoker.invoke(method, testInstance, extensionContext, registry, + ReflectiveInterceptorCall.ofVoidMethod(InvocationInterceptor::interceptBeforeAllMethod)); + } + catch (Throwable throwable) { + invokeBeforeAllExecutionExceptionHandlers(registry, extensionContext, throwable); + } + }); if (throwableCollector.isNotEmpty()) { break; } } } + private void invokeBeforeAllExecutionExceptionHandlers(ExtensionRegistry registry, ExtensionContext context, + Throwable throwable) { + + invokeExecutionExceptionHandlers(throwable, + registry.getReversedExtensions(LifecycleMethodExecutionExceptionHandler.class), + (ex, handler) -> () -> ((LifecycleMethodExecutionExceptionHandler) handler).handleBeforeAllMethodExecutionException( + context, ex)); + } + private void invokeAfterAllMethods(JupiterEngineExecutionContext context) { ExtensionRegistry registry = context.getExtensionRegistry(); ExtensionContext extensionContext = context.getExtensionContext(); ThrowableCollector throwableCollector = context.getThrowableCollector(); Object testInstance = extensionContext.getTestInstance().orElse(null); - this.afterAllMethods.forEach( - method -> throwableCollector.execute(() -> executableInvoker.invoke(method, testInstance, extensionContext, - registry, ReflectiveInterceptorCall.ofVoidMethod(InvocationInterceptor::interceptAfterAllMethod)))); + this.afterAllMethods.forEach(method -> throwableCollector.execute(() -> { + try { + executableInvoker.invoke(method, testInstance, extensionContext, registry, + ReflectiveInterceptorCall.ofVoidMethod(InvocationInterceptor::interceptAfterAllMethod)); + } + catch (Throwable throwable) { + invokeAfterAllExecutionExceptionHandlers(registry, extensionContext, throwable); + } + })); + } + + private void invokeAfterAllExecutionExceptionHandlers(ExtensionRegistry registry, ExtensionContext context, + Throwable throwable) { + + invokeExecutionExceptionHandlers(throwable, + registry.getReversedExtensions(LifecycleMethodExecutionExceptionHandler.class), + (ex, handler) -> () -> ((LifecycleMethodExecutionExceptionHandler) handler).handleAfterAllMethodExecutionException( + context, ex)); } private void invokeAfterAllCallbacks(JupiterEngineExecutionContext context) { diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/JupiterTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/JupiterTestDescriptor.java index dc04b05f75df..e371b6959b99 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/JupiterTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/JupiterTestDescriptor.java @@ -21,13 +21,16 @@ import java.lang.reflect.AnnotatedElement; import java.util.Collections; import java.util.LinkedHashSet; +import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.function.BiFunction; import java.util.function.Supplier; import org.apiguardian.api.API; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.extension.ConditionEvaluationResult; +import org.junit.jupiter.api.extension.Extension; import org.junit.jupiter.api.parallel.Execution; import org.junit.jupiter.api.parallel.ResourceAccessMode; import org.junit.jupiter.api.parallel.ResourceLock; @@ -37,6 +40,8 @@ import org.junit.platform.commons.JUnitException; import org.junit.platform.commons.logging.Logger; import org.junit.platform.commons.logging.LoggerFactory; +import org.junit.platform.commons.util.BlacklistedExceptions; +import org.junit.platform.commons.util.ExceptionUtils; import org.junit.platform.engine.TestDescriptor; import org.junit.platform.engine.TestSource; import org.junit.platform.engine.TestTag; @@ -45,6 +50,7 @@ import org.junit.platform.engine.support.hierarchical.ExclusiveResource; import org.junit.platform.engine.support.hierarchical.ExclusiveResource.LockMode; import org.junit.platform.engine.support.hierarchical.Node; +import org.junit.platform.engine.support.hierarchical.ThrowableCollector; /** * @since 5.0 @@ -95,6 +101,28 @@ protected static Set getTags(AnnotatedElement element) { // @formatter:on } + /** + * Invokes handlers on the {@code Throwable} one by one until none are left or the throwable to handle + * has been swallowed. + */ + void invokeExecutionExceptionHandlers(Throwable throwable, List handlers, + BiFunction generator) { + // No handlers left? + if (handlers.isEmpty()) { + ExceptionUtils.throwAsUncheckedException(throwable); + } + + try { + // Invoke next available handler + ThrowableCollector.Executable executable = generator.apply(throwable, handlers.remove(0)); + executable.execute(); + } + catch (Throwable handledThrowable) { + BlacklistedExceptions.rethrowIfBlacklisted(handledThrowable); + invokeExecutionExceptionHandlers(handledThrowable, handlers, generator); + } + } + // --- Node ---------------------------------------------------------------- @Override diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestMethodTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestMethodTestDescriptor.java index 19526d7a4e28..a36cb0579033 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestMethodTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestMethodTestDescriptor.java @@ -28,6 +28,7 @@ import org.junit.jupiter.api.extension.Extension; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.InvocationInterceptor; +import org.junit.jupiter.api.extension.LifecycleMethodExecutionExceptionHandler; import org.junit.jupiter.api.extension.TestExecutionExceptionHandler; import org.junit.jupiter.api.extension.TestInstances; import org.junit.jupiter.api.extension.TestWatcher; @@ -41,7 +42,6 @@ import org.junit.platform.commons.logging.Logger; import org.junit.platform.commons.logging.LoggerFactory; import org.junit.platform.commons.util.BlacklistedExceptions; -import org.junit.platform.commons.util.ExceptionUtils; import org.junit.platform.commons.util.ReflectionUtils; import org.junit.platform.engine.TestDescriptor; import org.junit.platform.engine.TestExecutionResult; @@ -154,9 +154,23 @@ private void invokeBeforeEachCallbacks(JupiterEngineExecutionContext context) { private void invokeBeforeEachMethods(JupiterEngineExecutionContext context) { ExtensionRegistry registry = context.getExtensionRegistry(); - invokeBeforeMethodsOrCallbacksUntilExceptionOccurs(context, - ((extensionContext, adapter) -> () -> adapter.invokeBeforeEachMethod(extensionContext, registry)), - BeforeEachMethodAdapter.class); + invokeBeforeMethodsOrCallbacksUntilExceptionOccurs(context, ((extensionContext, adapter) -> () -> { + try { + adapter.invokeBeforeEachMethod(extensionContext, registry); + } + catch (Throwable throwable) { + invokeBeforeEachExecutionExceptionHandlers(extensionContext, registry, throwable); + } + }), BeforeEachMethodAdapter.class); + } + + private void invokeBeforeEachExecutionExceptionHandlers(ExtensionContext context, ExtensionRegistry registry, + Throwable throwable) { + + invokeExecutionExceptionHandlers(throwable, + registry.getReversedExtensions(LifecycleMethodExecutionExceptionHandler.class), + (ex, handler) -> () -> ((LifecycleMethodExecutionExceptionHandler) handler).handleBeforeEachMethodExecutionException( + context, ex)); } private void invokeBeforeTestExecutionCallbacks(JupiterEngineExecutionContext context) { @@ -203,26 +217,9 @@ protected void invokeTestMethod(JupiterEngineExecutionContext context, DynamicTe private void invokeTestExecutionExceptionHandlers(ExtensionRegistry registry, ExtensionContext context, Throwable ex) { - invokeTestExecutionExceptionHandlers(ex, registry.getReversedExtensions(TestExecutionExceptionHandler.class), - context); - } - - private void invokeTestExecutionExceptionHandlers(Throwable ex, List handlers, - ExtensionContext context) { - - // No handlers left? - if (handlers.isEmpty()) { - ExceptionUtils.throwAsUncheckedException(ex); - } - - try { - // Invoke next available handler - handlers.remove(0).handleTestExecutionException(context, ex); - } - catch (Throwable t) { - BlacklistedExceptions.rethrowIfBlacklisted(t); - invokeTestExecutionExceptionHandlers(t, handlers, context); - } + invokeExecutionExceptionHandlers(ex, registry.getReversedExtensions(TestExecutionExceptionHandler.class), + (throwable, handler) -> () -> ((TestExecutionExceptionHandler) handler).handleTestExecutionException( + context, throwable)); } private void invokeAfterTestExecutionCallbacks(JupiterEngineExecutionContext context) { @@ -233,9 +230,24 @@ private void invokeAfterTestExecutionCallbacks(JupiterEngineExecutionContext con private void invokeAfterEachMethods(JupiterEngineExecutionContext context) { ExtensionRegistry registry = context.getExtensionRegistry(); - invokeAllAfterMethodsOrCallbacks(context, - ((extensionContext, adapter) -> () -> adapter.invokeAfterEachMethod(extensionContext, registry)), - AfterEachMethodAdapter.class); + invokeAllAfterMethodsOrCallbacks(context, ((extensionContext, adapter) -> () -> { + try { + adapter.invokeAfterEachMethod(extensionContext, registry); + } + catch (Throwable throwable) { + invokeAfterEachExecutionExceptionHandlers(extensionContext, registry, throwable); + } + }), AfterEachMethodAdapter.class); + } + + private void invokeAfterEachExecutionExceptionHandlers(ExtensionContext context, ExtensionRegistry registry, + Throwable throwable) { + + invokeExecutionExceptionHandlers(throwable, + registry.getReversedExtensions(LifecycleMethodExecutionExceptionHandler.class), + (ex, handler) -> () -> ((LifecycleMethodExecutionExceptionHandler) handler).handleAfterEachMethodExecutionException( + context, ex)); + } private void invokeAfterEachCallbacks(JupiterEngineExecutionContext context) { diff --git a/junit-jupiter-engine/src/test/java/org/junit/jupiter/api/extension/KitchenSinkExtension.java b/junit-jupiter-engine/src/test/java/org/junit/jupiter/api/extension/KitchenSinkExtension.java index 4c4d773ed12d..8e9592aea64c 100644 --- a/junit-jupiter-engine/src/test/java/org/junit/jupiter/api/extension/KitchenSinkExtension.java +++ b/junit-jupiter-engine/src/test/java/org/junit/jupiter/api/extension/KitchenSinkExtension.java @@ -40,6 +40,9 @@ public class KitchenSinkExtension implements AfterEachCallback, AfterAllCallback, + // Lifecycle methods exception handling + LifecycleMethodExecutionExceptionHandler, + // Dependency Injection TestInstanceFactory, TestInstancePostProcessor, @@ -88,6 +91,24 @@ public void afterEach(ExtensionContext context) { public void afterAll(ExtensionContext context) { } + // --- Lifecycle methods exception handling + + @Override + public void handleBeforeAllMethodExecutionException(ExtensionContext context, Throwable throwable) { + } + + @Override + public void handleBeforeEachMethodExecutionException(ExtensionContext context, Throwable throwable) { + } + + @Override + public void handleAfterEachMethodExecutionException(ExtensionContext context, Throwable throwable) { + } + + @Override + public void handleAfterAllMethodExecutionException(ExtensionContext context, Throwable throwable) { + } + // --- Dependency Injection ------------------------------------------------ @Override diff --git a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/LifecycleMethodExecutionExceptionHandlersTests.java b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/LifecycleMethodExecutionExceptionHandlersTests.java new file mode 100644 index 000000000000..992e6b1d1aac --- /dev/null +++ b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/LifecycleMethodExecutionExceptionHandlersTests.java @@ -0,0 +1,568 @@ +/* + * Copyright 2015-2019 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.jupiter.engine.extension; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; +import static org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder.request; +import static org.junit.platform.testkit.engine.EventConditions.container; +import static org.junit.platform.testkit.engine.EventConditions.engine; +import static org.junit.platform.testkit.engine.EventConditions.event; +import static org.junit.platform.testkit.engine.EventConditions.finishedSuccessfully; +import static org.junit.platform.testkit.engine.EventConditions.finishedWithFailure; +import static org.junit.platform.testkit.engine.EventConditions.started; +import static org.junit.platform.testkit.engine.EventConditions.test; +import static org.junit.platform.testkit.engine.TestExecutionResultConditions.instanceOf; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.LifecycleMethodExecutionExceptionHandler; +import org.junit.jupiter.engine.AbstractJupiterTestEngineTests; +import org.junit.platform.launcher.LauncherDiscoveryRequest; +import org.junit.platform.testkit.engine.EngineExecutionResults; + +/** + * Tests that verify the support for lifecycle methods execution exception handling + * via {@link LifecycleMethodExecutionExceptionHandler} + * + * @since 5.5 + */ +class LifecycleMethodExecutionExceptionHandlersTests extends AbstractJupiterTestEngineTests { + + private static List handlerCalls = new ArrayList<>(); + private static boolean throwExceptionBeforeAll; + private static boolean throwExceptionBeforeEach; + private static boolean throwExceptionAfterEach; + private static boolean throwExceptionAfterAll; + + @BeforeEach + void resetStatics() { + throwExceptionBeforeAll = true; + throwExceptionBeforeEach = true; + throwExceptionAfterEach = true; + throwExceptionAfterAll = true; + handlerCalls.clear(); + + SwallowExceptionHandler.beforeAllCalls = 0; + SwallowExceptionHandler.beforeEachCalls = 0; + SwallowExceptionHandler.afterEachCalls = 0; + SwallowExceptionHandler.afterAllCalls = 0; + + RethrowExceptionHandler.beforeAllCalls = 0; + RethrowExceptionHandler.beforeEachCalls = 0; + RethrowExceptionHandler.afterEachCalls = 0; + RethrowExceptionHandler.afterAllCalls = 0; + + ConvertExceptionHandler.beforeAllCalls = 0; + ConvertExceptionHandler.beforeEachCalls = 0; + ConvertExceptionHandler.afterEachCalls = 0; + ConvertExceptionHandler.afterAllCalls = 0; + + BlacklistedExceptionHandler.beforeAllCalls = 0; + BlacklistedExceptionHandler.beforeEachCalls = 0; + BlacklistedExceptionHandler.afterEachCalls = 0; + BlacklistedExceptionHandler.afterAllCalls = 0; + + ShouldNotBeCalledHandler.beforeAllCalls = 0; + ShouldNotBeCalledHandler.beforeEachCalls = 0; + ShouldNotBeCalledHandler.afterEachCalls = 0; + ShouldNotBeCalledHandler.afterAllCalls = 0; + } + + @Test + void classLevelExceptionHandlersRethrowException() { + LauncherDiscoveryRequest request = request().selectors(selectClass(RethrowingTestCase.class)).build(); + EngineExecutionResults executionResults = executeTests(request); + + assertEquals(1, RethrowExceptionHandler.beforeAllCalls, "Exception should handled in @BeforeAll"); + assertEquals(1, RethrowExceptionHandler.afterAllCalls, "Exception should handled in @AfterAll"); + + executionResults.all().assertEventsMatchExactly( // + event(engine(), started()), // + event(container(RethrowingTestCase.class), started()), // + event(container(RethrowingTestCase.class), finishedWithFailure(instanceOf(RuntimeException.class))), // + event(engine(), finishedSuccessfully())); + } + + @Test + void testLevelExceptionHandlersRethrowException() { + throwExceptionBeforeAll = false; + throwExceptionAfterAll = false; + LauncherDiscoveryRequest request = request().selectors(selectClass(RethrowingTestCase.class)).build(); + EngineExecutionResults executionResults = executeTests(request); + + assertEquals(1, RethrowExceptionHandler.beforeEachCalls, "Exception should be handled in @BeforeEach"); + assertEquals(1, RethrowExceptionHandler.afterEachCalls, "Exception should be handled in @AfterEach"); + + executionResults.all().assertEventsMatchExactly( // + event(engine(), started()), // + event(container(RethrowingTestCase.class), started()), // + event(test("aTest"), started()), // + event(test("aTest"), finishedWithFailure(instanceOf(RuntimeException.class))), // + event(container(RethrowingTestCase.class), finishedSuccessfully()), // + event(engine(), finishedSuccessfully())); + } + + @Test + void classLevelExceptionHandlersConvertException() { + LauncherDiscoveryRequest request = request().selectors(selectClass(ConvertingTestCase.class)).build(); + EngineExecutionResults executionResults = executeTests(request); + + assertEquals(1, ConvertExceptionHandler.beforeAllCalls, "Exception should handled in @BeforeAll"); + assertEquals(1, ConvertExceptionHandler.afterAllCalls, "Exception should handled in @AfterAll"); + + executionResults.all().assertEventsMatchExactly( // + event(engine(), started()), // + event(container(ConvertingTestCase.class), started()), // + event(container(ConvertingTestCase.class), finishedWithFailure(instanceOf(IOException.class))), // + event(engine(), finishedSuccessfully())); + } + + @Test + void testLevelExceptionHandlersConvertException() { + throwExceptionBeforeAll = false; + throwExceptionAfterAll = false; + LauncherDiscoveryRequest request = request().selectors(selectClass(ConvertingTestCase.class)).build(); + EngineExecutionResults executionResults = executeTests(request); + + assertEquals(1, ConvertExceptionHandler.beforeEachCalls, "Exception should be handled in @BeforeEach"); + assertEquals(1, ConvertExceptionHandler.afterEachCalls, "Exception should be handled in @AfterEach"); + + executionResults.all().assertEventsMatchExactly( // + event(engine(), started()), // + event(container(ConvertingTestCase.class), started()), // + event(test("aTest"), started()), // + event(test("aTest"), finishedWithFailure(instanceOf(IOException.class))), // + event(container(ConvertingTestCase.class), finishedSuccessfully()), // + event(engine(), finishedSuccessfully())); + } + + @Test + void exceptionHandlersSwallowException() { + LauncherDiscoveryRequest request = request().selectors(selectClass(SwallowingTestCase.class)).build(); + EngineExecutionResults executionResults = executeTests(request); + + assertEquals(1, SwallowExceptionHandler.beforeAllCalls, "Exception should be handled in @BeforeAll"); + assertEquals(1, SwallowExceptionHandler.beforeEachCalls, "Exception should be handled in @BeforeEach"); + assertEquals(1, SwallowExceptionHandler.afterEachCalls, "Exception should be handled in @AfterEach"); + assertEquals(1, SwallowExceptionHandler.afterAllCalls, "Exception should be handled in @AfterAll"); + + executionResults.all().assertEventsMatchExactly( // + event(engine(), started()), // + event(container(SwallowingTestCase.class), started()), // + event(test("aTest"), started()), // + event(test("aTest"), finishedSuccessfully()), // + event(container(SwallowingTestCase.class), finishedSuccessfully()), // + event(engine(), finishedSuccessfully())); + } + + @Test + void perClassLifecycleMethodsAreHandled() { + LauncherDiscoveryRequest request = request().selectors(selectClass(PerClassLifecycleTestCase.class)).build(); + EngineExecutionResults executionResults = executeTests(request); + assertEquals(2, SwallowExceptionHandler.beforeAllCalls, "Exception should be handled in @BeforeAll"); + assertEquals(1, SwallowExceptionHandler.beforeEachCalls, "Exception should be handled in @BeforeEach"); + assertEquals(1, SwallowExceptionHandler.afterEachCalls, "Exception should be handled in @AfterEach"); + assertEquals(2, SwallowExceptionHandler.afterAllCalls, "Exception should be handled in @AfterAll"); + + executionResults.all().assertEventsMatchExactly( // + event(engine(), started()), // + event(container(PerClassLifecycleTestCase.class), started()), // + event(test("aTest"), started()), // + event(test("aTest"), finishedSuccessfully()), // + event(container(PerClassLifecycleTestCase.class), finishedSuccessfully()), // + event(engine(), finishedSuccessfully())); + } + + @Test + void multipleHandlersAreCalledInOrder() { + LauncherDiscoveryRequest request = request().selectors(selectClass(MultipleHandlersTestCase.class)).build(); + EngineExecutionResults executionResults = executeTests(request); + + executionResults.all().assertEventsMatchExactly( // + event(engine(), started()), // + event(container(MultipleHandlersTestCase.class), started()), // + event(test("aTest"), started()), // + event(test("aTest"), finishedSuccessfully()), // + event(test("aTest2"), started()), // + event(test("aTest2"), finishedSuccessfully()), // + event(container(MultipleHandlersTestCase.class), finishedSuccessfully()), // + event(engine(), finishedSuccessfully())); // + + assertEquals(Arrays.asList( + // BeforeAll chain (class level only) + "RethrowExceptionBeforeAll", "SwallowExceptionBeforeAll", + // BeforeEach chain for aTest (test + class level) + "ConvertExceptionBeforeEach", "RethrowExceptionBeforeEach", "SwallowExceptionBeforeEach", + // AfterEach chain for aTest (test + class level) + "ConvertExceptionAfterEach", "RethrowExceptionAfterEach", "SwallowExceptionAfterEach", + // BeforeEach chain for aTest2 (class level only) + "RethrowExceptionBeforeEach", "SwallowExceptionBeforeEach", + // AfterEach chain for aTest2 (class level only) + "RethrowExceptionAfterEach", "SwallowExceptionAfterEach", + // AfterAll chain (class level only) + "RethrowExceptionAfterAll", "SwallowExceptionAfterAll" // + ), handlerCalls, "Wrong order of handler calls"); + } + + @Test + void blacklistedExceptionsAreNotPropagatedInBeforeAll() { + throwExceptionBeforeAll = true; + throwExceptionBeforeEach = false; + throwExceptionAfterEach = false; + throwExceptionAfterAll = false; + + boolean blackListedExceptionThrown = executeThrowingOutOfMemoryException(); + assertTrue(blackListedExceptionThrown, "Blacklisted Exception should be thrown"); + assertEquals(1, BlacklistedExceptionHandler.beforeAllCalls, "Exception should be handled in @BeforeAll"); + assertEquals(0, ShouldNotBeCalledHandler.beforeAllCalls, "Exception should not propagate in @BeforeAll"); + } + + @Test + void blacklistedExceptionsAreNotPropagatedInBeforeEach() { + throwExceptionBeforeAll = false; + throwExceptionBeforeEach = true; + throwExceptionAfterEach = false; + throwExceptionAfterAll = false; + + boolean blackListedExceptionThrown = executeThrowingOutOfMemoryException(); + assertTrue(blackListedExceptionThrown, "Blacklisted Exception should be thrown"); + assertEquals(1, BlacklistedExceptionHandler.beforeEachCalls, "Exception should be handled in @BeforeEach"); + assertEquals(0, ShouldNotBeCalledHandler.beforeEachCalls, "Exception should not propagate in @BeforeEach"); + } + + @Test + void blacklistedExceptionsAreNotPropagatedInAfterEach() { + throwExceptionBeforeAll = false; + throwExceptionBeforeEach = false; + throwExceptionAfterEach = true; + throwExceptionAfterAll = false; + + boolean blackListedExceptionThrown = executeThrowingOutOfMemoryException(); + assertTrue(blackListedExceptionThrown, "Blacklisted Exception should be thrown"); + assertEquals(1, BlacklistedExceptionHandler.afterEachCalls, "Exception should be handled in @AfterEach"); + assertEquals(0, ShouldNotBeCalledHandler.afterEachCalls, "Exception should not propagate in @AfterEach"); + } + + @Test + void blacklistedExceptionsAreNotPropagatedInAfterAll() { + throwExceptionBeforeAll = false; + throwExceptionBeforeEach = false; + throwExceptionAfterEach = false; + throwExceptionAfterAll = true; + + boolean blackListedExceptionThrown = executeThrowingOutOfMemoryException(); + assertTrue(blackListedExceptionThrown, "Blacklisted Exception should be thrown"); + assertEquals(1, BlacklistedExceptionHandler.afterAllCalls, "Exception should be handled in @AfterAll"); + assertEquals(0, ShouldNotBeCalledHandler.afterAllCalls, "Exception should not propagate in @AfterAll"); + } + + private boolean executeThrowingOutOfMemoryException() { + LauncherDiscoveryRequest request = request().selectors(selectClass(BlacklistedExceptionTestCase.class)).build(); + try { + executeTests(request); + } + catch (OutOfMemoryError expected) { + return true; + } + return false; + } + + // ------------------------------------------ + + static class BaseTestCase { + @BeforeAll + static void throwBeforeAll() { + if (throwExceptionBeforeAll) { + throw new RuntimeException("BeforeAllEx"); + } + } + + @BeforeEach + void throwBeforeEach() { + if (throwExceptionBeforeEach) { + throw new RuntimeException("BeforeEachEx"); + } + } + + @Test + void aTest() { + } + + @AfterEach + void throwAfterEach() { + if (throwExceptionAfterEach) { + throw new RuntimeException("AfterEachEx"); + } + } + + @AfterAll + static void throwAfterAll() { + if (throwExceptionAfterAll) { + throw new RuntimeException("AfterAllEx"); + } + } + } + + @ExtendWith(RethrowExceptionHandler.class) + static class RethrowingTestCase extends BaseTestCase { + } + + @ExtendWith(ConvertExceptionHandler.class) + static class ConvertingTestCase extends BaseTestCase { + } + + @ExtendWith(SwallowExceptionHandler.class) + static class SwallowingTestCase extends BaseTestCase { + } + + @ExtendWith(ShouldNotBeCalledHandler.class) + @ExtendWith(BlacklistedExceptionHandler.class) + static class BlacklistedExceptionTestCase extends BaseTestCase { + } + + @ExtendWith(ShouldNotBeCalledHandler.class) + @ExtendWith(SwallowExceptionHandler.class) + @ExtendWith(RethrowExceptionHandler.class) + @TestMethodOrder(MethodOrderer.OrderAnnotation.class) + static class MultipleHandlersTestCase extends BaseTestCase { + + @ExtendWith(ConvertExceptionHandler.class) + @Order(1) + @Test + void aTest() { + } + + @Order(2) + @Test + void aTest2() { + } + } + + @ExtendWith(SwallowExceptionHandler.class) + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + static class PerClassLifecycleTestCase extends BaseTestCase { + + @BeforeAll + void beforeAll() { + throw new RuntimeException("nonStaticBeforeAllEx"); + } + + @AfterAll + void afterAll() { + throw new RuntimeException("nonStaticAfterAllEx"); + } + } + + // ------------------------------------------ + + static class RethrowExceptionHandler implements LifecycleMethodExecutionExceptionHandler { + static int beforeAllCalls = 0; + static int beforeEachCalls = 0; + static int afterEachCalls = 0; + static int afterAllCalls = 0; + + @Override + public void handleBeforeAllMethodExecutionException(ExtensionContext context, Throwable throwable) + throws Throwable { + beforeAllCalls++; + handlerCalls.add("RethrowExceptionBeforeAll"); + throw throwable; + } + + @Override + public void handleBeforeEachMethodExecutionException(ExtensionContext context, Throwable throwable) + throws Throwable { + beforeEachCalls++; + handlerCalls.add("RethrowExceptionBeforeEach"); + throw throwable; + } + + @Override + public void handleAfterEachMethodExecutionException(ExtensionContext context, Throwable throwable) + throws Throwable { + afterEachCalls++; + handlerCalls.add("RethrowExceptionAfterEach"); + throw throwable; + } + + @Override + public void handleAfterAllMethodExecutionException(ExtensionContext context, Throwable throwable) + throws Throwable { + afterAllCalls++; + handlerCalls.add("RethrowExceptionAfterAll"); + throw throwable; + } + } + + static class SwallowExceptionHandler implements LifecycleMethodExecutionExceptionHandler { + static int beforeAllCalls = 0; + static int beforeEachCalls = 0; + static int afterEachCalls = 0; + static int afterAllCalls = 0; + + @Override + public void handleBeforeAllMethodExecutionException(ExtensionContext context, Throwable throwable) { + beforeAllCalls++; + handlerCalls.add("SwallowExceptionBeforeAll"); + // Do not rethrow + } + + @Override + public void handleBeforeEachMethodExecutionException(ExtensionContext context, Throwable throwable) { + beforeEachCalls++; + handlerCalls.add("SwallowExceptionBeforeEach"); + // Do not rethrow + } + + @Override + public void handleAfterEachMethodExecutionException(ExtensionContext context, Throwable throwable) { + afterEachCalls++; + handlerCalls.add("SwallowExceptionAfterEach"); + // Do not rethrow + } + + @Override + public void handleAfterAllMethodExecutionException(ExtensionContext context, Throwable throwable) { + afterAllCalls++; + handlerCalls.add("SwallowExceptionAfterAll"); + // Do not rethrow + } + } + + static class ConvertExceptionHandler implements LifecycleMethodExecutionExceptionHandler { + static int beforeAllCalls = 0; + static int beforeEachCalls = 0; + static int afterEachCalls = 0; + static int afterAllCalls = 0; + + @Override + public void handleBeforeAllMethodExecutionException(ExtensionContext context, Throwable throwable) + throws Throwable { + beforeAllCalls++; + handlerCalls.add("ConvertExceptionBeforeAll"); + throw new IOException(throwable); + } + + @Override + public void handleBeforeEachMethodExecutionException(ExtensionContext context, Throwable throwable) + throws Throwable { + beforeEachCalls++; + handlerCalls.add("ConvertExceptionBeforeEach"); + throw new IOException(throwable); + } + + @Override + public void handleAfterEachMethodExecutionException(ExtensionContext context, Throwable throwable) + throws Throwable { + afterEachCalls++; + handlerCalls.add("ConvertExceptionAfterEach"); + throw new IOException(throwable); + } + + @Override + public void handleAfterAllMethodExecutionException(ExtensionContext context, Throwable throwable) + throws Throwable { + afterAllCalls++; + handlerCalls.add("ConvertExceptionAfterAll"); + throw new IOException(throwable); + } + } + + static class BlacklistedExceptionHandler implements LifecycleMethodExecutionExceptionHandler { + static int beforeAllCalls = 0; + static int beforeEachCalls = 0; + static int afterEachCalls = 0; + static int afterAllCalls = 0; + + @Override + public void handleBeforeAllMethodExecutionException(ExtensionContext context, Throwable throwable) { + beforeAllCalls++; + handlerCalls.add("BlacklistedExceptionBeforeAll"); + throw new OutOfMemoryError(); + } + + @Override + public void handleBeforeEachMethodExecutionException(ExtensionContext context, Throwable throwable) { + beforeEachCalls++; + handlerCalls.add("BlacklistedExceptionBeforeEach"); + throw new OutOfMemoryError(); + } + + @Override + public void handleAfterEachMethodExecutionException(ExtensionContext context, Throwable throwable) { + afterEachCalls++; + handlerCalls.add("BlacklistedExceptionAfterEach"); + throw new OutOfMemoryError(); + } + + @Override + public void handleAfterAllMethodExecutionException(ExtensionContext context, Throwable throwable) { + afterAllCalls++; + handlerCalls.add("BlacklistedExceptionAfterAll"); + throw new OutOfMemoryError(); + } + } + + static class ShouldNotBeCalledHandler implements LifecycleMethodExecutionExceptionHandler { + static int beforeAllCalls = 0; + static int beforeEachCalls = 0; + static int afterEachCalls = 0; + static int afterAllCalls = 0; + + @Override + public void handleBeforeAllMethodExecutionException(ExtensionContext context, Throwable throwable) + throws Throwable { + beforeAllCalls++; + handlerCalls.add("ShouldNotBeCalledBeforeAll"); + throw throwable; + } + + @Override + public void handleBeforeEachMethodExecutionException(ExtensionContext context, Throwable throwable) + throws Throwable { + ShouldNotBeCalledHandler.beforeEachCalls++; + handlerCalls.add("ShouldNotBeCalledBeforeEach"); + throw throwable; + } + + @Override + public void handleAfterEachMethodExecutionException(ExtensionContext context, Throwable throwable) + throws Throwable { + afterEachCalls++; + handlerCalls.add("ShouldNotBeCalledAfterEach"); + throw throwable; + } + + @Override + public void handleAfterAllMethodExecutionException(ExtensionContext context, Throwable throwable) + throws Throwable { + afterAllCalls++; + handlerCalls.add("ShouldNotBeCalledAfterAll"); + throw throwable; + } + } +}