CombinatorialTestExtension.java
/*
* #%L
* wcm.io
* %%
* Copyright (C) 2020 wcm.io
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package io.wcm.qa.glnm.junit.combinatorial;
import static io.wcm.qa.glnm.junit.combinatorial.CombinatorialUtil.extractArguments;
import static io.wcm.qa.glnm.junit.combinatorial.CombinatorialUtil.extractExtensionSources;
import static java.util.stream.Collectors.toList;
import static org.junit.jupiter.params.CombinatorialTestInvocationContext.invocationContext;
import static org.junit.jupiter.params.CombinatorialTestNameFormatter.formatter;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Stream;
import org.junit.jupiter.api.extension.Extension;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
import org.junit.jupiter.api.extension.TestTemplateInvocationContext;
import org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider;
import org.junit.jupiter.params.CombinatorialTestMethodContext;
import org.junit.jupiter.params.CombinatorialTestNameFormatter;
import org.junit.platform.commons.support.AnnotationSupport;
import org.junit.platform.commons.util.Preconditions;
abstract class CombinatorialTestExtension implements TestTemplateInvocationContextProvider {
private static final String METHOD_CONTEXT_KEY = "context";
protected CombinatorialTestExtension() {
super();
}
/** {@inheritDoc} */
@Override
public Stream<TestTemplateInvocationContext> provideTestTemplateInvocationContexts(ExtensionContext extensionContext) {
Method templateMethod = extensionContext.getRequiredTestMethod();
String displayName = extensionContext.getDisplayName();
CombinatorialTestMethodContext methodContext = getMethodContext(extensionContext);
CombinatorialTestNameFormatter formatter = createNameFormatter(templateMethod, displayName, methodContext);
AtomicLong invocationCount = new AtomicLong(0);
return provideCombinations(extensionContext)
.map(combination -> createInvocationContext(formatter, methodContext, combination))
.peek(invocationContext -> invocationCount.incrementAndGet())
.onClose(() -> Preconditions.condition(invocationCount.get() > 0,
"Configuration error: You must configure at least one multiplier for this combinatorial test"));
}
/** {@inheritDoc} */
@Override
public boolean supportsTestTemplate(ExtensionContext context) {
if (!context.getTestMethod().isPresent()) {
return false;
}
Method testMethod = context.getTestMethod().get();
if (!supportsTestMethod(testMethod)) {
return false;
}
CombinatorialTestMethodContext methodContext = CombinatorialTestMethodContext.forMethod(testMethod);
Preconditions.condition(methodContext.hasPotentiallyValidSignature(),
() -> String.format(
"@ParameterizedTest method [%s] declares formal parameters in an invalid order: "
+ "argument aggregators must be declared after any indexed arguments "
+ "and before any arguments resolved by another ParameterResolver.",
testMethod.toGenericString()));
getStore(context).put(METHOD_CONTEXT_KEY, methodContext);
return true;
}
private List<List<Combinable>> collectExtensions(ExtensionContext context) {
List<CombinableProvider> extensionSources = extractExtensionSources(context);
return extensionSources.stream()
.map(CombinableProvider::combinables)
.collect(toList());
}
private TestTemplateInvocationContext createInvocationContext(
CombinatorialTestNameFormatter formatter,
CombinatorialTestMethodContext methodContext,
Combination combination) {
Object[] arguments = combination.arguments().get();
Object[] consumedArguments = CombinatorialUtil.consumedArguments(arguments, methodContext);
List<Extension> extensions = combination.extensions();
return invocationContext(formatter, methodContext, consumedArguments, extensions);
}
private CombinatorialTestNameFormatter createNameFormatter(
Method templateMethod,
String displayName,
CombinatorialTestMethodContext methodContext) {
String pattern = getNamePattern(templateMethod);
return formatter(pattern, displayName, methodContext);
}
private CombinatorialTestMethodContext getMethodContext(ExtensionContext extensionContext) {
return getStore(extensionContext)//
.get(METHOD_CONTEXT_KEY, CombinatorialTestMethodContext.class);
}
private boolean supportsTestMethod(Method testMethod) {
return AnnotationSupport.isAnnotated(testMethod, getAnnotationClass());
}
protected abstract Stream<Combination> combine(List<List<Combinable>> list);
protected abstract Class<? extends Annotation> getAnnotationClass();
protected abstract String getNamePattern(Method templateMethod);
protected Stream<Combination> provideCombinations(ExtensionContext extensionContext) {
List<List<Combinable>> arguments = extractArguments(extensionContext);
List<List<Combinable>> extensions = collectExtensions(extensionContext);
return combine(
Stream.of(arguments, extensions)
.flatMap(Collection::stream)
.collect(toList()));
}
ExtensionContext.Store getStore(ExtensionContext context) {
return context.getStore(Namespace.create(this.getClass(), context.getRequiredTestMethod()));
}
}