/**
* 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.
*/
package org.deephacks.confit.test;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import org.deephacks.confit.spi.Lookup;
import org.junit.runners.Parameterized.Parameters;
import org.scannotation.AnnotationDB;
import org.scannotation.ClasspathUrlFinder;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* {@link FeatureTestsBuilder} is to be used by {@link FeatureTests} to build a set of
* feature tests.
*/
public abstract class FeatureTestsBuilder {
/** name of the test suite as shown in junit test execution */
protected String name;
/** Run before every test */
private Map<Class<?>, Object> setUp = new LinkedHashMap<>();
/** Run after every test */
private Map<Class<?>, Object> tearDown = new LinkedHashMap<>();
/** TestSetupTeardown classes found on classpath */
private static final Map<String, Class<?>> setupTeardownRegistry = new HashMap<>();
/** TestSetupTeardown to run for each test */
private static final LinkedHashMap<String, Class<?>> setupTeardowns = new LinkedHashMap<>();
/** TestSetupTeardown that are parameterized to run for each test */
private static final Map<Class<?>, Method> parameterizedMethods = new HashMap<>();
private static final Lookup lookup = Lookup.get();
static {
// search classpath for @TestSetupTeardown annotated classes
findSetupTeardownClasses();
}
/**
* Set implementation for a specific service interface. Will automatically
* enforce any setup/teardown requirements that the implementation might have
* by searching classpath for TestSetupTeardown annotated classes.
*/
public <T> FeatureTestsBuilder using(Class<T> service, T impl) {
// make sure Lookup.lookup().lookup() find the the right implementation.
lookup.register(service, impl);
Class<?> setupTeardown = setupTeardownRegistry.get(service.getName());
if (setupTeardown != null) {
// make sure implementation setup/teardown is run.
Method m = getParameterizedMethod(setupTeardown);
if (m != null) {
parameterizedMethods.put(setupTeardown, m);
} else {
setupTeardowns.put(setupTeardown.getName(), setupTeardown);
}
}
return this;
}
/**
* Add setup classes to be run @Before
*/
public FeatureTestsBuilder withSetUp(Object setUp) {
this.setUp.put(setUp.getClass(), setUp);
return this;
}
protected Map<Class<?>, Object> getSetUp() {
return setUp;
}
/**
* Add teardown classes to be run @After
*/
public FeatureTestsBuilder withTearDown(Object tearDown) {
this.tearDown.put(tearDown.getClass(), setUp);
return this;
}
protected Map<Class<?>, Object> getTearDown() {
return tearDown;
}
protected abstract List<Class<?>> getTests();
public List<TestRound> build() {
final List<Class<?>> testClasses = getTests();
List<TestRound> rounds = new ArrayList<>();
for (Class<?> test : testClasses) {
ArrayList<Object> setups = new ArrayList<>();
try {
for (Class<?> setupTeardown : setupTeardowns.values()) {
setups.add(newInstance(setupTeardown));
}
for (Class<?> setupTeardown : parameterizedMethods.keySet()) {
Method m = parameterizedMethods.get(setupTeardown);
@SuppressWarnings("unchecked")
Collection<Object[]> argMatrix = (Collection<Object[]>) m.invoke(null,
(Object[]) null);
for (Object[] args : argMatrix) {
Object setup = newInstance(setupTeardown, args);
ArrayList<Object> roundSetups = new ArrayList<>(setups);
roundSetups.add(setup);
rounds.add(new TestRound(test, args[0].toString(), roundSetups));
}
}
} catch (Exception e) {
throw new RuntimeException(e);
}
if (parameterizedMethods.size() == 0) {
rounds.add(new TestRound(test, setups));
}
}
return rounds;
}
public Object newInstance(Class<?> cls) {
try {
return cls.newInstance();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public Object newInstance(Class<?> cls, Object[] args) {
try {
return getConstructor(cls).newInstance(args);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private Constructor<?> getConstructor(Class<?> setupTeardown) {
for (Constructor<?> c : setupTeardown.getConstructors()) {
if (c.getParameterTypes().length == 1) {
return c;
}
}
throw new RuntimeException("Parameterized class must have a constructor with one argument.");
}
private Method getParameterizedMethod(Class<?> setupTeardown) {
for (Method m : setupTeardown.getDeclaredMethods()) {
try {
if (m.getAnnotation(Parameters.class) != null) {
return m;
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
return null;
}
/**
* Search classpath and init TestSetupTeardown classes.
*/
private static void findSetupTeardownClasses() {
AnnotationDB db = new AnnotationDB();
try {
URL[] urls = ClasspathUrlFinder.findClassPaths();
db.scanArchives(urls);
Map<String, Set<String>> annotationIndex = db.getAnnotationIndex();
for (String cls : annotationIndex.get(FeatureSetupTeardown.class.getName())) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
Class<?> setup = cl.loadClass(cls);
FeatureSetupTeardown anno = setup.getAnnotation(FeatureSetupTeardown.class);
if (setupTeardownRegistry.get(anno.value().getName()) != null) {
if (!cls.toLowerCase().contains("default")) {
setupTeardownRegistry.put(anno.value().getName(), setup);
}
} else {
setupTeardownRegistry.put(anno.value().getName(), setup);
}
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static class TestRound {
private Optional<String> name;
private Class<?> testClass;
private List<Object> setupTeardowns = new ArrayList<>();
public TestRound(Class<?> testClass, String name, List<Object> setupTeardowns) {
Preconditions.checkNotNull(testClass);
Preconditions.checkNotNull(name);
Preconditions.checkNotNull(setupTeardowns);
this.name = Optional.of(name);
this.setupTeardowns.addAll(setupTeardowns);
this.testClass = testClass;
}
public TestRound(Class<?> cls, List<Object> setupTeardowns) {
this.name = Optional.absent();
this.setupTeardowns.addAll(setupTeardowns);
this.testClass = cls;
}
public Optional<String> getName() {
return name;
}
public Class<?> getTestClass() {
return testClass;
}
public List<Object> getSetupTeardowns() {
return setupTeardowns;
}
}
}