blob: a7bf1c0d3b3686ec5de3e42e3fc5a36ebe8b70eb [file] [log] [blame]
/*****************************************************************************
* Copyright (c) 2015 Christian W. Damus and others.
*
* 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
*
* Contributors:
* Christian W. Damus - Initial API and implementation
*
*****************************************************************************/
package org.eclipse.papyrus.junit.framework.classification;
import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Collections;
import java.util.Deque;
import java.util.Iterator;
import java.util.List;
import org.junit.runner.Description;
import org.junit.runner.Runner;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.Suite;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.RunnerBuilder;
import org.junit.runners.model.Statement;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.AbstractIterator;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.ObjectArrays;
/**
* A specialized Test {@linkplain Suite Suite} runner that recognizes the Papyrus-specific
* {@link ClassificationConfig classification} annotations on the suite as a whole. It also
* supports a dynamic specification of test suites to include via the {@literal @}{@link DynamicClasses}
* annotation, especially useful for generated tests that may or may not have been generated at
* the time of test execution (or at least of compilation of the test suite class).
*/
public class ClassificationSuite extends Suite {
/**
* Mapping of whether any tests at all in a suite tree are statically enabled, according to
* their annotations.
*/
private static final LoadingCache<Description, Boolean> enabledSuites = CacheBuilder.newBuilder().build(
CacheLoader.from(ClassificationSuite::isEnabled));
private Description description;
public ClassificationSuite(Class<?> klass, RunnerBuilder builder) throws InitializationError {
this(builder, klass, getAnnotatedClasses(klass));
}
public ClassificationSuite(Class<?> klass, Class<?>[] suiteClasses) throws InitializationError {
super(klass, withDynamicSuites(klass, suiteClasses));
}
public ClassificationSuite(RunnerBuilder builder, Class<?> klass, Class<?>[] suiteClasses) throws InitializationError {
super(builder, klass, withDynamicSuites(klass, suiteClasses));
}
private static Class<?>[] getAnnotatedClasses(Class<?> class_) throws InitializationError {
SuiteClasses annotation = class_.getAnnotation(SuiteClasses.class);
if (annotation == null) {
throw new InitializationError(String.format("class '%s' must have a SuiteClasses annotation", class_.getName()));
}
return annotation.value();
}
private static Class<?>[] withDynamicSuites(Class<?> suiteClass, Class<?>[] staticSuites) {
Class<?>[] result = staticSuites;
Class<?>[] dynamicSuites = getDynamicSuites(suiteClass);
if (dynamicSuites.length > 0) {
result = ObjectArrays.concat(staticSuites, dynamicSuites, Class.class);
}
return result;
}
private static Class<?>[] getDynamicSuites(Class<?> suiteClass) {
List<Class<?>> result;
DynamicClasses dynclasses = suiteClass.getAnnotation(DynamicClasses.class);
if (dynclasses == null) {
result = Collections.emptyList();
} else {
result = Lists.newArrayListWithCapacity(dynclasses.value().length);
for (String classname : dynclasses.value()) {
try {
result.add(suiteClass.getClassLoader().loadClass(classname));
} catch (Exception e) {
// OK. It's not there, so we just ignore it
}
}
}
return Iterables.toArray(result, Class.class);
}
@Override
protected List<Runner> getChildren() {
// If I don't match the current configuration, none of my tests will run, so don't even provide them
// because they just inflate the size of the progress meter
if (ClassificationConfig.shouldRun(getRunnerAnnotations())) {
return super.getChildren();
} else {
return Collections.emptyList();
}
}
@Override
public void run(RunNotifier notifier) {
// If I don't match the current configuration, none of my tests should be run
if (ClassificationConfig.shouldRun(getRunnerAnnotations())) {
super.run(notifier);
} else {
Description description = getDescription();
notifier.fireTestIgnored(description);
}
}
@Override
protected Statement classBlock(final RunNotifier notifier) {
// We never throw in these lazy calculations
return enabledSuites.getUnchecked(getDescription())
// Include the @BeforeClass, @AfterClass, and @ClassRule steps
? super.classBlock(notifier)
// Just the bare bones to record everything that is skipped
: skipAll(notifier);
}
@Override
public Description getDescription() {
if (description == null) {
description = super.getDescription();
}
return description;
}
/**
* Creates a statement that simply skips all of the tests in my suite, recursively.
*
* @param notifier
* tracks the execution (or, rather, skipping) of the tests
*
* @return the all-skipping statement
*/
protected Statement skipAll(RunNotifier notifier) {
return new Statement() {
@Override
public void evaluate() {
skipAll(getDescription(), notifier);
}
};
}
/**
* Skips all of the tests in the given {@code suite}, recursively.
*
* @param suite
* a test suite to skip
* @param notifier
* tracks the execution (or, rather, skipping) of the tests
*/
protected void skipAll(Description suite, RunNotifier notifier) {
for (Description next : suite.getChildren()) {
if (next.isSuite()) {
skipAll(next, notifier);
} else {
notifier.fireTestIgnored(next);
}
}
notifier.fireTestIgnored(suite);
}
/**
* Queries whether a given {@code suite} will run any tests at all.
*
* @param suite
* a test suite
* @return whether it has any leaf tests that will not be ignored for some reason
*/
static boolean isEnabled(Description suite) {
boolean result = false;
for (Description leaf : getLeafTests(suite)) {
Iterable<Annotation> annotations = Iterables.concat(
leaf.getAnnotations(),
Arrays.asList(leaf.getTestClass().getAnnotations()));
if (ClassificationConfig.shouldRun(Iterables.toArray(annotations, Annotation.class))) {
result = true;
break;
}
}
return result;
}
/**
* Obtains an iteration of all of the leaf-level tests in a {@code suite}, in depth-first order.
*
* @param suite
* a test suite to iterate
*
* @return all of its leaf-level tests
*/
static Iterable<Description> getLeafTests(Description suite) {
return new Iterable<Description>() {
@Override
public Iterator<Description> iterator() {
return new AbstractIterator<Description>() {
Deque<Iterator<Description>> stack = new ArrayDeque<>();
Iterator<Description> current = suite.getChildren().iterator();
// Advance the tractor
Iterator<Description> feed() {
while (!current.hasNext()) {
current = stack.pollLast();
if (current == null) {
current = Collections.emptyIterator();
break;
}
}
return current;
}
void push(Description suite) {
stack.addLast(current);
current = suite.getChildren().iterator();
}
@Override
protected Description computeNext() {
Description result = null;
while ((result == null) && feed().hasNext()) {
Description next = feed().next();
if (next.isTest() && (next.getTestClass() != null)) {
result = next;
} else if (next.isSuite()) {
// Push this suite onto the stack
push(next);
} else {
// Otherwise it's a weird test without a class?!?
System.err.println("Leaf test without a class: " + next);
}
}
if (result == null) {
result = endOfData();
}
return result;
}
};
}
};
}
//
// Nested types
//
/**
* An annotation like the {@literal @}{@link SuiteClasses} that specifies, by name, test suite classes
* to find and include dynamically. Any that are not available are simply ignored.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface DynamicClasses {
/**
* Names of test suite classes to optionally include in the test suite.
*/
String[] value();
}
}