/*
* Copyright 2015-2017 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 v1.0 which
* accompanies this distribution and is available at
*
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.junit.platform.runner;
import static java.util.Arrays.stream;
import static java.util.stream.Collectors.toList;
import static org.junit.platform.commons.meta.API.Usage.Maintained;
import static org.junit.platform.engine.discovery.ClassNameFilter.STANDARD_INCLUDE_PATTERN;
import static org.junit.platform.engine.discovery.ClassNameFilter.excludeClassNamePatterns;
import static org.junit.platform.engine.discovery.ClassNameFilter.includeClassNamePatterns;
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
import static org.junit.platform.engine.discovery.PackageNameFilter.excludePackageNames;
import static org.junit.platform.engine.discovery.PackageNameFilter.includePackageNames;
import static org.junit.platform.launcher.EngineFilter.excludeEngines;
import static org.junit.platform.launcher.EngineFilter.includeEngines;
import static org.junit.platform.launcher.TagFilter.excludeTags;
import static org.junit.platform.launcher.TagFilter.includeTags;
import static org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder.request;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
import org.junit.platform.commons.meta.API;
import org.junit.platform.commons.util.Preconditions;
import org.junit.platform.engine.DiscoverySelector;
import org.junit.platform.engine.discovery.DiscoverySelectors;
import org.junit.platform.launcher.Launcher;
import org.junit.platform.launcher.LauncherDiscoveryRequest;
import org.junit.platform.launcher.TestIdentifier;
import org.junit.platform.launcher.TestPlan;
import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
import org.junit.platform.launcher.core.LauncherFactory;
import org.junit.platform.suite.api.ExcludeClassNamePatterns;
import org.junit.platform.suite.api.ExcludeEngines;
import org.junit.platform.suite.api.ExcludePackages;
import org.junit.platform.suite.api.ExcludeTags;
import org.junit.platform.suite.api.IncludeClassNamePatterns;
import org.junit.platform.suite.api.IncludeEngines;
import org.junit.platform.suite.api.IncludePackages;
import org.junit.platform.suite.api.IncludeTags;
import org.junit.platform.suite.api.SelectClasses;
import org.junit.platform.suite.api.SelectPackages;
import org.junit.platform.suite.api.UseTechnicalNames;
import org.junit.runner.Description;
import org.junit.runner.Runner;
import org.junit.runner.manipulation.Filter;
import org.junit.runner.manipulation.Filterable;
import org.junit.runner.manipulation.NoTestsRemainException;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.model.InitializationError;
/**
* JUnit 4 based {@link Runner} which runs tests on the JUnit Platform in a
* JUnit 4 environment.
*
* <p>Annotating a class with {@code @RunWith(JUnitPlatform.class)} allows it
* to be run with IDEs and build systems that support JUnit 4 but do not yet
* support the JUnit Platform directly.
*
* <p>Consult the various annotations in this package for configuration options.
*
* <p>If you do not use any configuration annotations from this package, you
* can simply use this runner on a test class whose programming model is
* supported on the JUnit Platform — for example, a JUnit Jupiter test class.
* Note, however, that any test class run with this runner must be {@code public}
* in order to be picked up by IDEs and build tools.
*
* <p>When used on a class that serves as a test suite and the
* {@link IncludeClassNamePatterns @IncludeClassNamePatterns} annotation is not
* present, the default include pattern
* {@value org.junit.platform.engine.discovery.ClassNameFilter#STANDARD_INCLUDE_PATTERN}
* will be used in order to avoid loading classes unnecessarily (see {@link
* org.junit.platform.engine.discovery.ClassNameFilter#STANDARD_INCLUDE_PATTERN
* ClassNameFilter#STANDARD_INCLUDE_PATTERN}).
*
* @since 1.0
* @see SelectPackages
* @see SelectClasses
* @see IncludeClassNamePatterns
* @see ExcludeClassNamePatterns
* @see IncludeTags
* @see ExcludeTags
* @see IncludeEngines
* @see ExcludeEngines
* @see UseTechnicalNames
*/
@API(Maintained)
public class JUnitPlatform extends Runner implements Filterable {
private static final Class<?>[] EMPTY_CLASS_ARRAY = new Class<?>[0];
private static final String[] EMPTY_STRING_ARRAY = new String[0];
private final Class<?> testClass;
private final Launcher launcher;
private LauncherDiscoveryRequest discoveryRequest;
private JUnitPlatformTestTree testTree;
public JUnitPlatform(Class<?> testClass) throws InitializationError {
this(testClass, LauncherFactory.create());
}
// For testing only
JUnitPlatform(Class<?> testClass, Launcher launcher) throws InitializationError {
this.launcher = launcher;
this.testClass = testClass;
this.discoveryRequest = createDiscoveryRequest();
this.testTree = generateTestTree();
}
@Override
public Description getDescription() {
return this.testTree.getSuiteDescription();
}
@Override
public void run(RunNotifier notifier) {
JUnitPlatformRunnerListener listener = new JUnitPlatformRunnerListener(this.testTree, notifier);
this.launcher.registerTestExecutionListeners(listener);
this.launcher.execute(this.discoveryRequest);
}
private JUnitPlatformTestTree generateTestTree() {
Preconditions.notNull(this.discoveryRequest, "DiscoveryRequest must not be null");
TestPlan plan = this.launcher.discover(this.discoveryRequest);
return new JUnitPlatformTestTree(plan, testClass);
}
private LauncherDiscoveryRequest createDiscoveryRequest() {
List<DiscoverySelector> selectors = getSelectorsFromAnnotations();
// Allows to simply add @RunWith(JUnitPlatform.class) to any test case
boolean isSuite = !selectors.isEmpty();
if (!isSuite) {
selectors.add(selectClass(this.testClass));
}
LauncherDiscoveryRequestBuilder requestBuilder = request().selectors(selectors);
addFiltersFromAnnotations(requestBuilder, isSuite);
return requestBuilder.build();
}
private void addFiltersFromAnnotations(LauncherDiscoveryRequestBuilder requestBuilder, boolean isSuite) {
addIncludeClassNamePatternFilter(requestBuilder, isSuite);
addExcludeClassNamePatternFilter(requestBuilder);
addIncludePackagesFilter(requestBuilder);
addExcludePackagesFilter(requestBuilder);
addIncludedTagsFilter(requestBuilder);
addExcludedTagsFilter(requestBuilder);
addIncludedEnginesFilter(requestBuilder);
addExcludedEnginesFilter(requestBuilder);
}
private List<DiscoverySelector> getSelectorsFromAnnotations() {
List<DiscoverySelector> selectors = new ArrayList<>();
selectors.addAll(transform(getSelectedClasses(), DiscoverySelectors::selectClass));
selectors.addAll(transform(getSelectedPackageNames(), DiscoverySelectors::selectPackage));
return selectors;
}
private <T> List<DiscoverySelector> transform(T[] sourceElements, Function<T, DiscoverySelector> transformer) {
return stream(sourceElements).map(transformer).collect(toList());
}
private void addIncludeClassNamePatternFilter(LauncherDiscoveryRequestBuilder requestBuilder, boolean isSuite) {
String[] patterns = getIncludeClassNamePatterns(isSuite);
if (patterns.length > 0) {
requestBuilder.filters(includeClassNamePatterns(patterns));
}
}
private void addExcludeClassNamePatternFilter(LauncherDiscoveryRequestBuilder requestBuilder) {
String[] patterns = getExcludeClassNamePatterns();
if (patterns.length > 0) {
requestBuilder.filters(excludeClassNamePatterns(patterns));
}
}
private void addIncludePackagesFilter(LauncherDiscoveryRequestBuilder requestBuilder) {
String[] includedPackages = getIncludedPackages();
if (includedPackages.length > 0) {
requestBuilder.filters(includePackageNames(includedPackages));
}
}
private void addExcludePackagesFilter(LauncherDiscoveryRequestBuilder requestBuilder) {
String[] excludedPackages = getExcludedPackages();
if (excludedPackages.length > 0) {
requestBuilder.filters(excludePackageNames(excludedPackages));
}
}
private void addIncludedTagsFilter(LauncherDiscoveryRequestBuilder requestBuilder) {
String[] includedTags = getIncludedTags();
if (includedTags.length > 0) {
requestBuilder.filters(includeTags(includedTags));
}
}
private void addExcludedTagsFilter(LauncherDiscoveryRequestBuilder requestBuilder) {
String[] excludedTags = getExcludedTags();
if (excludedTags.length > 0) {
requestBuilder.filters(excludeTags(excludedTags));
}
}
private void addIncludedEnginesFilter(LauncherDiscoveryRequestBuilder requestBuilder) {
String[] engineIds = getIncludedEngineIds();
if (engineIds.length > 0) {
requestBuilder.filters(includeEngines(engineIds));
}
}
private void addExcludedEnginesFilter(LauncherDiscoveryRequestBuilder requestBuilder) {
String[] engineIds = getExcludedEngineIds();
if (engineIds.length > 0) {
requestBuilder.filters(excludeEngines(engineIds));
}
}
private Class<?>[] getSelectedClasses() {
return getValueFromAnnotation(SelectClasses.class, SelectClasses::value, EMPTY_CLASS_ARRAY);
}
private String[] getSelectedPackageNames() {
return getValueFromAnnotation(SelectPackages.class, SelectPackages::value, EMPTY_STRING_ARRAY);
}
private String[] getIncludedPackages() {
return getValueFromAnnotation(IncludePackages.class, IncludePackages::value, EMPTY_STRING_ARRAY);
}
private String[] getExcludedPackages() {
return getValueFromAnnotation(ExcludePackages.class, ExcludePackages::value, EMPTY_STRING_ARRAY);
}
private String[] getIncludedTags() {
return getValueFromAnnotation(IncludeTags.class, IncludeTags::value, EMPTY_STRING_ARRAY);
}
private String[] getExcludedTags() {
return getValueFromAnnotation(ExcludeTags.class, ExcludeTags::value, EMPTY_STRING_ARRAY);
}
private String[] getIncludedEngineIds() {
return getValueFromAnnotation(IncludeEngines.class, IncludeEngines::value, EMPTY_STRING_ARRAY);
}
private String[] getExcludedEngineIds() {
return getValueFromAnnotation(ExcludeEngines.class, ExcludeEngines::value, EMPTY_STRING_ARRAY);
}
private String[] getIncludeClassNamePatterns(boolean isSuite) {
String[] patterns = getValueFromAnnotation(IncludeClassNamePatterns.class, IncludeClassNamePatterns::value,
new String[0]);
if (patterns.length == 0 && isSuite) {
return new String[] { STANDARD_INCLUDE_PATTERN };
}
Preconditions.containsNoNullElements(patterns, "IncludeClassNamePatterns must not contain null elements");
trim(patterns);
return patterns;
}
private String[] getExcludeClassNamePatterns() {
String[] patterns = getValueFromAnnotation(ExcludeClassNamePatterns.class, ExcludeClassNamePatterns::value,
new String[0]);
Preconditions.containsNoNullElements(patterns, "ExcludeClassNamePatterns must not contain null elements");
trim(patterns);
return patterns;
}
private void trim(String[] patterns) {
for (int i = 0; i < patterns.length; i++) {
patterns[i] = patterns[i].trim();
}
}
private <A extends Annotation, V> V getValueFromAnnotation(Class<A> annotationClass, Function<A, V> extractor,
V defaultValue) {
A annotation = this.testClass.getAnnotation(annotationClass);
return (annotation != null ? extractor.apply(annotation) : defaultValue);
}
@Override
public void filter(Filter filter) throws NoTestsRemainException {
Set<TestIdentifier> filteredIdentifiers = testTree.getFilteredLeaves(filter);
if (filteredIdentifiers.isEmpty()) {
throw new NoTestsRemainException();
}
this.discoveryRequest = createDiscoveryRequestForUniqueIds(filteredIdentifiers);
this.testTree = generateTestTree();
}
private LauncherDiscoveryRequest createDiscoveryRequestForUniqueIds(Set<TestIdentifier> testIdentifiers) {
// @formatter:off
List<DiscoverySelector> selectors = testIdentifiers.stream()
.map(TestIdentifier::getUniqueId)
.map(DiscoverySelectors::selectUniqueId)
.collect(toList());
// @formatter:on
return request().selectors(selectors).build();
}
}