Skip to content

Commit

Permalink
Added CombineTestTemplate support
Browse files Browse the repository at this point in the history
This adds support for combining multiple TestTemplate providers
together, in a product style, so that an implementer can use the
benefits of multiple extensions together.

Issue: junit-team#1224
  • Loading branch information
danielhodder committed Sep 15, 2020
1 parent 7aebb42 commit 237b94d
Show file tree
Hide file tree
Showing 2 changed files with 127 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright 2015-2020 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;

import static org.apiguardian.api.API.Status.EXPERIMENTAL;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.apiguardian.api.API;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.TestTemplateInvocationContext;
import org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider;
import org.junit.platform.commons.annotation.Testable;

/**
* Opt this test method into combining {@link TestTemplateInvocationContextProvider} instances together into a single
* one. This means that the product of the tests will be run. This allows combining tests such as the {
* @link RepeatedTest} and {@code ParameterizedTest} annotations.
* <p>
* <b>Note:</b> Not all {@link TestTemplateInvocationContextProvider} instances may be compatible with this.
* Specifically: any implementation of {@link TestTemplateInvocationContextProvider} which does complex initilization
* logic in the {@link TestTemplateInvocationContextProvider#provideTestTemplateInvocationContexts(ExtensionContext)}
* method may not work. Extenstions that wish to support this option should instead to their work in an extension
* that is created, new, each time {@link TestTemplateInvocationContext#getAdditionalExtensions()} is called.
* </p>
*/
@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@API(status = EXPERIMENTAL, since = "5.8")
@Testable
public @interface CombineTestTemplates {
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,16 @@
import static org.junit.jupiter.engine.descriptor.ExtensionUtils.populateNewExtensionRegistryFromExtendWithAnnotation;

import java.lang.reflect.Method;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.apiguardian.api.API;
import org.junit.jupiter.api.CombineTestTemplates;
import org.junit.jupiter.api.extension.Extension;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.TestInstances;
import org.junit.jupiter.api.extension.TestTemplateInvocationContext;
Expand All @@ -29,6 +34,7 @@
import org.junit.jupiter.engine.execution.JupiterEngineExecutionContext;
import org.junit.jupiter.engine.extension.ExtensionRegistry;
import org.junit.jupiter.engine.extension.MutableExtensionRegistry;
import org.junit.platform.commons.support.AnnotationSupport;
import org.junit.platform.commons.util.Preconditions;
import org.junit.platform.engine.TestDescriptor;
import org.junit.platform.engine.UniqueId;
Expand Down Expand Up @@ -98,9 +104,21 @@ public JupiterEngineExecutionContext execute(JupiterEngineExecutionContext conte
List<TestTemplateInvocationContextProvider> providers = validateProviders(extensionContext,
context.getExtensionRegistry());
AtomicInteger invocationIndex = new AtomicInteger();

boolean productTestTemplates = AnnotationSupport.isAnnotated(extensionContext.getElement(),
CombineTestTemplates.class);
Stream<TestTemplateInvocationContext> invocationContexts;
if (productTestTemplates) {
invocationContexts = productOfTestTemplateContexts(providers, extensionContext).map(
AdaptingTestTemplateExecutionContext::new);
}
else {
invocationContexts = providers.stream().flatMap(
provider -> provider.provideTestTemplateInvocationContexts(extensionContext));
}

// @formatter:off
providers.stream()
.flatMap(provider -> provider.provideTestTemplateInvocationContexts(extensionContext))
invocationContexts
.map(invocationContext -> createInvocationTestDescriptor(invocationContext, invocationIndex.incrementAndGet()))
.filter(Optional::isPresent)
.map(Optional::get)
Expand Down Expand Up @@ -149,4 +167,66 @@ private void validateWasAtLeastInvokedOnce(int invocationIndex,
+ " provided a non-empty stream");
}

/**
* Allows the creation of a product of {@link TestTemplateInvocationContext TestTemplateInvocationContext} from a
* list of providers. Each provider will be asked for execution contexts and those will be combined.
* <p>
* If there are two Executors (for example the {@code RepeatedTestExtension} and the
* {@code ParameterizedTestExtension} and they provide templates [1, 2, 3] and [A, B] respectively; then this
* method will return a stream of the following items:
* </p>
* <ul>
* <li>[1, A]</li>
* <li>[1, B]</li>
* <li>[2, A]</li>
* <li>[2, B]</li>
* <li>[3, A<]</li>
* <li>[3, B]</li>
* </ul>
* <p>
* The intention here is that this can then be passed to {@link AdaptingTestTemplateExecutionContext} to be executed.
* </p>
*
* @param providers the providers to use to generate the template contexts
* @param extensionContext the extension context to use when generating the test contexts
* @return a stream of test invocation context combinations
*/
private static Stream<List<TestTemplateInvocationContext>> productOfTestTemplateContexts(
List<TestTemplateInvocationContextProvider> providers, ExtensionContext extensionContext) {
if (providers.isEmpty()) {
return Stream.of(Collections.emptyList());
}

TestTemplateInvocationContextProvider firstProvider = providers.get(0);
List<TestTemplateInvocationContextProvider> tail = providers.subList(1, providers.size());

return firstProvider.provideTestTemplateInvocationContexts(extensionContext).flatMap(
context -> productOfTestTemplateContexts(tail, extensionContext).map(
contexts -> Stream.concat(Stream.of(context), contexts.stream()).collect(toList())));
}

/**
* Adapt a List of {@link TestTemplateInvocationContext TestTemplateInvocationContexts} into a single one for
* execution. The methods on {@link TestTemplateInvocationContext} delegate to each of the wrapped elements in turn.
*/
private static class AdaptingTestTemplateExecutionContext implements TestTemplateInvocationContext {
private final List<TestTemplateInvocationContext> delegates;

private AdaptingTestTemplateExecutionContext(List<TestTemplateInvocationContext> delegates) {
this.delegates = delegates;
}

@Override
public List<Extension> getAdditionalExtensions() {
return delegates.stream().flatMap(context -> context.getAdditionalExtensions().stream()).collect(
Collectors.toList());
}

@Override
public String getDisplayName(int invocationIndex) {
return delegates.stream().map(context -> context.getDisplayName(invocationIndex)).collect(
Collectors.joining(", ")) + " [Combined "+this.delegates.size()+" contexts. Execution "+invocationIndex+"]";
}
}

}

0 comments on commit 237b94d

Please sign in to comment.