diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/TestTemplateInvocationContextProvider.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/TestTemplateInvocationContextProvider.java index e978e174660..48efd489397 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/TestTemplateInvocationContextProvider.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/TestTemplateInvocationContextProvider.java @@ -13,6 +13,7 @@ import static org.junit.platform.commons.meta.API.Usage.Experimental; import java.util.Iterator; +import java.util.stream.Stream; import org.junit.platform.commons.meta.API; @@ -67,15 +68,19 @@ public interface TestTemplateInvocationContextProvider extends Extension { *

This method is only called by the framework if {@link #supports} has * previously returned {@code true} for the same * {@link ContainerExtensionContext}. Thus, it must not return an empty - * {@code Iterator}. + * {@code Stream}. + * + *

The returned {@code Stream} will be properly closed by calling + * {@link Stream#close()}, making it safe to use a resource such as + * {@link java.nio.file.Files#lines(java.nio.file.Path) Files.lines()}. * * @param context the container extension context for the test template * method about to be invoked; never {@code null} - * @return an Iterator of TestTemplateInvocationContext instances for the + * @return a Stream of TestTemplateInvocationContext instances for the * invocation of the test template method; never {@code null} or empty * @see #supports * @see ContainerExtensionContext */ - Iterator provide(ContainerExtensionContext context); + Stream provide(ContainerExtensionContext context); } diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestTemplateTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestTemplateTestDescriptor.java index aa840626828..78b9f7ea880 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestTemplateTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestTemplateTestDescriptor.java @@ -14,7 +14,6 @@ import static org.junit.platform.commons.meta.API.Usage.Internal; import java.lang.reflect.Method; -import java.util.Iterator; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; @@ -91,15 +90,12 @@ public JupiterEngineExecutionContext execute(JupiterEngineExecutionContext conte List providers = validateProviders(containerExtensionContext, context.getExtensionRegistry()); AtomicInteger invocationIndex = new AtomicInteger(); - providers.forEach(provider -> { - Iterator contextIterator = provider.provide(containerExtensionContext); - contextIterator.forEachRemaining(invocationContext -> { - int index = invocationIndex.incrementAndGet(); - TestDescriptor invocationTestDescriptor = createInvocationTestDescriptor(invocationContext, index); - addChild(invocationTestDescriptor); - dynamicTestExecutor.execute(invocationTestDescriptor); - }); - }); + // @formatter:off + providers.stream() + .flatMap(provider -> provider.provide(containerExtensionContext)) + .map(invocationContext -> createInvocationTestDescriptor(invocationContext, invocationIndex.incrementAndGet())) + .forEach(invocationTestDescriptor -> execute(dynamicTestExecutor, invocationTestDescriptor)); + // @formatter:on validateWasAtLeastInvokedOnce(invocationIndex); return context; } @@ -126,6 +122,11 @@ private TestDescriptor createInvocationTestDescriptor(TestTemplateInvocationCont invocationContext, index); } + private void execute(DynamicTestExecutor dynamicTestExecutor, TestDescriptor testDescriptor) { + addChild(testDescriptor); + dynamicTestExecutor.execute(testDescriptor); + } + private void validateWasAtLeastInvokedOnce(AtomicInteger invocationIndex) { if (invocationIndex.get() == 0) { throw new TestAbortedException("No supporting " diff --git a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/TestTemplateInvocationTests.java b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/TestTemplateInvocationTests.java index a6ccf1f6349..5c5de18cabc 100644 --- a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/TestTemplateInvocationTests.java +++ b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/TestTemplateInvocationTests.java @@ -11,7 +11,6 @@ package org.junit.jupiter.engine; import static java.util.Arrays.asList; -import static java.util.Collections.emptyIterator; import static java.util.Collections.singleton; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; @@ -38,8 +37,8 @@ import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; -import java.util.Iterator; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Predicate; import java.util.stream.Stream; @@ -77,7 +76,7 @@ /** * @since 5.0 */ -public class TestTemplateInvocationTests extends AbstractJupiterTestEngineTests { +class TestTemplateInvocationTests extends AbstractJupiterTestEngineTests { @Test void templateWithoutRegisteredExtensionReportsFailure() { @@ -311,6 +310,25 @@ void templateWithSupportingProviderButNoInvocationsReportsAbortedTest() { message("No supporting TestTemplateInvocationContextProvider provided an invocation context"))))); } + @Test + void templateWithCloseableStream() { + LauncherDiscoveryRequest request = request().selectors( + selectMethod(MyTestTemplateTestCase.class, "templateWithCloseableStream")).build(); + + ExecutionEventRecorder eventRecorder = executeTests(request); + + assertThat(InvocationContextProviderWithCloseableStream.streamClosed.get()).describedAs( + "streamClosed").isTrue(); + + assertRecordedExecutionEventsContainsExactly(eventRecorder.getExecutionEvents(), // + wrappedInContainerEvents(MyTestTemplateTestCase.class, // + event(container("templateWithCloseableStream"), started()), // + event(dynamicTestRegistered("test-template-invocation:#1")), // + event(test("test-template-invocation:#1"), started()), // + event(test("test-template-invocation:#1"), finishedSuccessfully()), // + event(container("templateWithCloseableStream"), finishedSuccessfully()))); + } + private TestDescriptor findTestDescriptor(ExecutionEventRecorder eventRecorder, Condition condition) { // @formatter:off @@ -338,10 +356,6 @@ private final Condition[] wrappedInContainerEvents(Class static class MyTestTemplateTestCase { - @Test - void foo() { - } - @TestTemplate void templateWithoutRegisteredExtension() { } @@ -392,10 +406,10 @@ void templateWithDynamicParameterResolver(String parameter) { @ExtendWith(StringParameterResolvingInvocationContextProvider.class) @TestTemplate void templateWithWrongParameterType(int parameter) { - fail("never called"); + fail("never called: " + parameter); } - private String parameterInstanceVariable; + String parameterInstanceVariable; @ExtendWith(StringParameterInjectingInvocationContextProvider.class) @TestTemplate @@ -409,6 +423,10 @@ void templateWithSupportingProviderButNoInvocations() { fail("never called"); } + @ExtendWith(InvocationContextProviderWithCloseableStream.class) + @TestTemplate + void templateWithCloseableStream() { + } } static class TestTemplateTestClassWithBeforeAndAfterEach { @@ -452,8 +470,8 @@ public boolean supports(ContainerExtensionContext context) { } @Override - public Iterator provide(ContainerExtensionContext context) { - return singleton(emptyTestTemplateInvocationContext()).iterator(); + public Stream provide(ContainerExtensionContext context) { + return Stream.of(emptyTestTemplateInvocationContext()); } } @@ -466,8 +484,8 @@ public boolean supports(ContainerExtensionContext context) { } @Override - public Iterator provide(ContainerExtensionContext context) { - return singleton(emptyTestTemplateInvocationContext()).iterator(); + public Stream provide(ContainerExtensionContext context) { + return Stream.of(emptyTestTemplateInvocationContext()); } } @@ -479,8 +497,8 @@ public boolean supports(ContainerExtensionContext context) { } @Override - public Iterator provide(ContainerExtensionContext context) { - return asList(emptyTestTemplateInvocationContext(), emptyTestTemplateInvocationContext()).iterator(); + public Stream provide(ContainerExtensionContext context) { + return Stream.of(emptyTestTemplateInvocationContext(), emptyTestTemplateInvocationContext()); } } @@ -507,13 +525,13 @@ public boolean supports(ContainerExtensionContext context) { } @Override - public Iterator provide(ContainerExtensionContext context) { + public Stream provide(ContainerExtensionContext context) { return Stream. generate(() -> new TestTemplateInvocationContext() { @Override public String getDisplayName(int invocationIndex) { return invocationIndex + " --> " + context.getDisplayName(); } - }).limit(1).iterator(); + }).limit(1); } } @@ -532,8 +550,8 @@ public boolean supports(ContainerExtensionContext context) { } @Override - public Iterator provide(ContainerExtensionContext context) { - return asList(createContext("foo"), createContext("bar")).iterator(); + public Stream provide(ContainerExtensionContext context) { + return Stream.of(createContext("foo"), createContext("bar")); } private TestTemplateInvocationContext createContext(String argument) { @@ -572,8 +590,8 @@ public boolean supports(ContainerExtensionContext context) { } @Override - public Iterator provide(ContainerExtensionContext context) { - return asList(createContext("foo"), createContext("bar")).iterator(); + public Stream provide(ContainerExtensionContext context) { + return Stream.of(createContext("foo"), createContext("bar")); } private TestTemplateInvocationContext createContext(String argument) { @@ -604,8 +622,8 @@ public boolean supports(ContainerExtensionContext context) { } @Override - public Iterator provide(ContainerExtensionContext context) { - return asList(createContext("foo"), createContext("bar")).iterator(); + public Stream provide(ContainerExtensionContext context) { + return Stream.of(createContext("foo"), createContext("bar")); } private TestTemplateInvocationContext createContext(String argument) { @@ -639,7 +657,7 @@ public void beforeTestExecution(TestExtensionContext context) throws Exception { public void handleTestExecutionException(TestExtensionContext context, Throwable throwable) throws Throwable { TestTemplateTestClassWithDynamicLifecycleCallbacks.lifecycleEvents.add("handleTestExecutionException"); - throw throwable; + throw new AssertionError(throwable); } @Override @@ -663,8 +681,23 @@ public boolean supports(ContainerExtensionContext context) { } @Override - public Iterator provide(ContainerExtensionContext context) { - return emptyIterator(); + public Stream provide(ContainerExtensionContext context) { + return Stream.empty(); + } + } + + private static class InvocationContextProviderWithCloseableStream implements TestTemplateInvocationContextProvider { + + private static AtomicBoolean streamClosed = new AtomicBoolean(false); + + @Override + public boolean supports(ContainerExtensionContext context) { + return true; + } + + @Override + public Stream provide(ContainerExtensionContext context) { + return Stream.of(emptyTestTemplateInvocationContext()).onClose(() -> streamClosed.set(true)); } } diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestExtension.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestExtension.java index 6d156ec85ed..e8df228a8d2 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestExtension.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestExtension.java @@ -16,7 +16,6 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Method; -import java.util.Iterator; import java.util.List; import java.util.Spliterator; import java.util.Spliterators; @@ -52,7 +51,7 @@ public boolean supports(ContainerExtensionContext context) { } @Override - public Iterator provide(ContainerExtensionContext context) { + public Stream provide(ContainerExtensionContext context) { Method templateMethod = Preconditions.notNull(context.getTestMethod().orElse(null), "test method must not be null"); ParameterizedTestNameFormatter formatter = createNameFormatter(templateMethod); @@ -64,8 +63,7 @@ public Iterator provide(ContainerExtensionContext .peek(provider -> initialize(templateMethod, provider)) .flatMap(ParameterizedTestExtension::toArgumentsStream) .map(Arguments::getArguments) - .map(arguments -> toTestTemplateInvocationContext(formatter, arguments)) - .iterator(); + .map(arguments -> toTestTemplateInvocationContext(formatter, arguments)); // @formatter:on }