package mil.nga.giat.geowave.test;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TimeZone;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.junit.internal.runners.statements.RunAfters;
import org.junit.internal.runners.statements.RunBefores;
import org.junit.runner.Runner;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.BlockJUnit4ClassRunner;
import org.junit.runners.Suite;
import org.junit.runners.model.FrameworkField;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.Statement;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import mil.nga.giat.geowave.core.store.operations.remote.options.DataStorePluginOptions;
import mil.nga.giat.geowave.test.annotation.Environments;
import mil.nga.giat.geowave.test.annotation.Environments.Environment;
import mil.nga.giat.geowave.test.annotation.GeoWaveTestStore;
import mil.nga.giat.geowave.test.annotation.GeoWaveTestStore.GeoWaveStoreType;
import mil.nga.giat.geowave.test.annotation.GeoWaveTestStoreImpl;
import mil.nga.giat.geowave.test.annotation.NamespaceOverride;
import mil.nga.giat.geowave.test.annotation.OptionsOverride;
public class GeoWaveITRunner extends
Suite
{
private static final Logger LOGGER = LoggerFactory.getLogger(GeoWaveITRunner.class);
public static final AtomicBoolean DEFER_CLEANUP = new AtomicBoolean(
false);
public static final Object MUTEX = new Object();
public static final String STORE_TYPE_ENVIRONMENT_VARIABLE_NAME = "STORE_TYPE";
public static final String STORE_TYPE_PROPERTY_NAME = "testStoreType";
@Override
protected Statement withBeforeClasses(
final Statement statement ) {
// add test environment setup
try {
final Method setupMethod = GeoWaveITRunner.class.getDeclaredMethod("setup");
setupMethod.setAccessible(true);
return super.withBeforeClasses(new RunBefores(
statement,
Collections.singletonList(new FrameworkMethod(
setupMethod)),
this));
}
catch (NoSuchMethodException | SecurityException e) {
LOGGER.warn(
"Unable to find setup method",
e);
}
return super.withBeforeClasses(statement);
}
@Override
protected Statement withAfterClasses(
final Statement statement ) {
// add test environment tear down
try {
final Statement newStatement = super.withAfterClasses(statement);
final Method tearDownMethod = GeoWaveITRunner.class.getDeclaredMethod("tearDown");
tearDownMethod.setAccessible(true);
return new RunAfters(
newStatement,
Collections.singletonList(new FrameworkMethod(
tearDownMethod)),
this);
}
catch (NoSuchMethodException | SecurityException e) {
LOGGER.warn(
"Unable to find tearDown method",
e);
}
return super.withAfterClasses(statement);
}
private class TestClassRunnerForStoreTypes extends
BlockJUnit4ClassRunner
{
private final Map<String, GeoWaveStoreType> fieldNameStoreTypePair;
private final String nameSuffix;
private TestClassRunnerForStoreTypes(
final Class<?> type,
final Map<String, GeoWaveStoreType> fieldNameStoreTypePair )
throws InitializationError {
super(
type);
this.fieldNameStoreTypePair = fieldNameStoreTypePair;
final StringBuilder nameBldr = new StringBuilder();
for (final Entry<String, GeoWaveStoreType> e : fieldNameStoreTypePair.entrySet()) {
nameBldr.append(
" (").append(
e.getKey()).append(
"=").append(
e.getValue().toString()).append(
")");
}
nameSuffix = nameBldr.toString();
}
@Override
public Object createTest()
throws Exception {
return createTestUsingFieldInjection();
}
private Object createTestUsingFieldInjection()
throws IllegalAccessException,
SecurityException,
NoSuchFieldException,
GeoWaveITException,
InstantiationException {
final Set<Pair<Field, GeoWaveTestStore>> fieldsAndStorePairs = new HashSet<Pair<Field, GeoWaveTestStore>>();
if (typeIsAnnotated()) {
final GeoWaveTestStore store = getTestClass().getJavaClass().getAnnotation(
GeoWaveTestStore.class);
for (final String fieldName : fieldNameStoreTypePair.keySet()) {
final Field field = getTestClass().getJavaClass().getDeclaredField(
fieldName);
final GeoWaveTestStoreImpl storeWithOverrides = new GeoWaveTestStoreImpl(
store);
if (field.isAnnotationPresent(NamespaceOverride.class)) {
storeWithOverrides.setNamespace(field.getAnnotation(
NamespaceOverride.class).value());
}
else if (field.isAnnotationPresent(OptionsOverride.class)) {
storeWithOverrides.setOptions(field.getAnnotation(
OptionsOverride.class).value());
}
fieldsAndStorePairs.add(new ImmutablePair<Field, GeoWaveTestStore>(
field,
storeWithOverrides));
}
}
else {
final List<FrameworkField> annotatedFields = getStoreAnnotatedFields();
if (annotatedFields.size() != fieldNameStoreTypePair.size()) {
throw new GeoWaveITException(
"Wrong number of stores and @GeoWaveTestStore fields."
+ " @GeoWaveTestStore fields counted: " + annotatedFields.size()
+ ", available parameters: " + fieldNameStoreTypePair.size() + ".");
}
for (final FrameworkField field : annotatedFields) {
fieldsAndStorePairs.add(new ImmutablePair<Field, GeoWaveTestStore>(
field.getField(),
field.getField().getAnnotation(
GeoWaveTestStore.class)));
}
}
final Object testClassInstance = getTestClass().getJavaClass().newInstance();
for (final Pair<Field, GeoWaveTestStore> field : fieldsAndStorePairs) {
final GeoWaveStoreType type = fieldNameStoreTypePair.get(field.getLeft().getName());
field.getLeft().setAccessible(
true);
final GeoWaveTestStore store = field.getRight();
field.getLeft().set(
testClassInstance,
type.getTestEnvironment().getDataStoreOptions(
store));
}
return testClassInstance;
}
@Override
protected String getName() {
return super.getName() + nameSuffix;
}
@Override
protected String testName(
final FrameworkMethod method ) {
return method.getName() + " - " + getName();
}
@Override
protected void validateFields(
final List<Throwable> errors ) {
super.validateFields(errors);
if (typeIsAnnotated()) {
if (fieldsAreAnnotated()) {
errors.add(new GeoWaveITException(
"Only type or fields can be annotated with @GeoWaveTestStore, not both"));
}
try {
getDataStoreOptionFieldsForTypeAnnotation();
}
catch (final Exception e) {
errors.add(e);
}
}
else if (fieldsAreAnnotated()) {
final List<FrameworkField> annotatedFields = getStoreAnnotatedFields();
for (final FrameworkField field : annotatedFields) {
if (!field.getType().isAssignableFrom(
DataStorePluginOptions.class)) {
errors.add(new GeoWaveITException(
"'" + field.getName() + "' must be of type '" + DataStorePluginOptions.class.getName()
+ "'"));
}
}
}
}
@Override
protected Statement classBlock(
final RunNotifier notifier ) {
return childrenInvoker(notifier);
}
@Override
protected Annotation[] getRunnerAnnotations() {
return new Annotation[0];
}
}
private static final List<Runner> NO_RUNNERS = Collections.<Runner> emptyList();
private final List<Runner> runners = new ArrayList<Runner>();
private final Set<GeoWaveStoreType> storeTypes = new HashSet<GeoWaveStoreType>();
private final TestEnvironment[] testEnvs;
/**
* Only called reflectively. Do not use programmatically.
*/
public GeoWaveITRunner(
final Class<?> klass )
throws InitializationError,
SecurityException,
GeoWaveITException {
super(
klass,
NO_RUNNERS);
createRunnersForDataStores();
testEnvs = getTestEnvironments();
}
@Override
protected List<Runner> getChildren() {
return runners;
}
private void createRunnersForDataStores()
throws InitializationError,
SecurityException,
GeoWaveITException {
final GeoWaveStoreRunnerConfig emptyConfig = new GeoWaveStoreRunnerConfig();
List<GeoWaveStoreRunnerConfig> configs = new ArrayList<GeoWaveStoreRunnerConfig>();
String storeTypeProp = System.getenv(STORE_TYPE_ENVIRONMENT_VARIABLE_NAME);
if (!TestUtils.isSet(storeTypeProp)) {
storeTypeProp = System.getProperty(STORE_TYPE_PROPERTY_NAME);
}
// See if user specified a single store type
if (TestUtils.isSet(storeTypeProp)) {
final Set<String> dataStoreOptionFields = getDataStoreOptionFieldsForTypeAnnotation();
final GeoWaveStoreType storeType = GeoWaveStoreType.valueOf(storeTypeProp);
// If no match, the configs list will be empty and the IT will be a
// no-op
if (containsAnnotationForType(storeType)) {
configs.add(new GeoWaveStoreRunnerConfig(
storeType,
dataStoreOptionFields));
storeTypes.add(storeType);
}
}
else { // No user override - just use the IT's list of types
if (typeIsAnnotated()) {
if (fieldsAreAnnotated()) {
throw new GeoWaveITException(
"Only type or fields can be annotated with @GeoWaveTestStore, not both");
}
final Set<String> dataStoreOptionFields = getDataStoreOptionFieldsForTypeAnnotation();
final GeoWaveTestStore store = getTestClass().getJavaClass().getAnnotation(
GeoWaveTestStore.class);
for (final GeoWaveStoreType storeType : store.value()) {
configs.add(new GeoWaveStoreRunnerConfig(
storeType,
dataStoreOptionFields));
storeTypes.add(storeType);
}
}
else {
configs.add(emptyConfig);
final List<FrameworkField> storeFields = getStoreAnnotatedFields();
for (final FrameworkField field : storeFields) {
configs = addRunnerConfigsForField(
field,
configs,
storeTypes);
}
}
}
// Create a test runner for each store type / config
for (final GeoWaveStoreRunnerConfig config : configs) {
final TestClassRunnerForStoreTypes runner = new TestClassRunnerForStoreTypes(
getTestClass().getJavaClass(),
config.fieldNameStoreTypePair);
runners.add(runner);
}
}
private boolean containsAnnotationForType(
final GeoWaveStoreType storeType ) {
if (typeIsAnnotated()) {
final GeoWaveTestStore store = getTestClass().getJavaClass().getAnnotation(
GeoWaveTestStore.class);
for (final GeoWaveStoreType annotationType : store.value()) {
if (annotationType == storeType) {
return true;
}
}
}
else {
for (final FrameworkField field : getTestClass().getAnnotatedFields(
GeoWaveTestStore.class)) {
for (final GeoWaveStoreType annotationType : field.getField().getAnnotation(
GeoWaveTestStore.class).value()) {
if (annotationType == storeType) {
return true;
}
}
}
}
return false;
}
private Set<String> getDataStoreOptionFieldsForTypeAnnotation()
throws SecurityException,
GeoWaveITException {
final Field[] fields = getTestClass().getJavaClass().getDeclaredFields();
final Set<String> dataStoreOptionFields = new HashSet<String>();
for (final Field field : fields) {
if (field.getType().isAssignableFrom(
DataStorePluginOptions.class)) {
dataStoreOptionFields.add(field.getName());
}
}
if (dataStoreOptionFields.isEmpty()) {
throw new GeoWaveITException(
"Types annotated with GeoWaveTestStore must have at least one field of type DataStorePluginOptions");
}
return dataStoreOptionFields;
}
private static List<GeoWaveStoreRunnerConfig> addRunnerConfigsForField(
final FrameworkField field,
final List<GeoWaveStoreRunnerConfig> currentConfigs,
final Set<GeoWaveStoreType> storeTypes )
throws GeoWaveITException {
final GeoWaveTestStore store = field.getField().getAnnotation(
GeoWaveTestStore.class);
final GeoWaveStoreType[] types = store.value();
if ((types == null) || (types.length == 0)) {
throw new GeoWaveITException(
MessageFormat.format(
"{0} must have at least one GeoWaveStoreType",
field.getName()));
}
final List<GeoWaveStoreRunnerConfig> newConfigs = new ArrayList<GeoWaveStoreRunnerConfig>();
for (final GeoWaveStoreRunnerConfig config : currentConfigs) {
for (final GeoWaveStoreType type : types) {
newConfigs.add(new GeoWaveStoreRunnerConfig(
config,
field.getName(),
type));
storeTypes.add(type);
}
}
return newConfigs;
}
private List<FrameworkField> getStoreAnnotatedFields() {
return getTestClass().getAnnotatedFields(
GeoWaveTestStore.class);
}
private List<FrameworkMethod> getTestEnvAnnotatedMethods() {
return getTestClass().getAnnotatedMethods(
Environments.class);
}
private TestEnvironment[] getTestEnvironments()
throws NullPointerException {
final Set<Environment> environments = new HashSet<Environment>();
final Environments es = getTestClass().getJavaClass().getAnnotation(
Environments.class);
if (es != null) {
final Environment[] envs = es.value();
for (final Environment env : envs) {
environments.add(env);
}
}
final List<FrameworkMethod> envMethods = getTestEnvAnnotatedMethods();
for (final FrameworkMethod m : envMethods) {
final Environment[] envs = m.getMethod().getAnnotation(
Environments.class).value();
for (final Environment env : envs) {
environments.add(env);
}
}
final TestEnvironment[] testEnvs = new TestEnvironment[environments.size() + storeTypes.size()];
int i = 0;
for (final GeoWaveStoreType t : storeTypes) {
testEnvs[i++] = t.getTestEnvironment();
}
for (final Environment e : environments) {
testEnvs[i++] = e.getTestEnvironment();
}
return processDependencies(testEnvs);
}
private TestEnvironment[] processDependencies(
final TestEnvironment[] testEnvs ) {
final TestEnvironmentDependencyTree dependencyTree = new TestEnvironmentDependencyTree();
for (final TestEnvironment e : testEnvs) {
dependencyTree.processDependencies(e);
}
return dependencyTree.getOrderedTestEnvironments();
}
private boolean fieldsAreAnnotated() {
return !getStoreAnnotatedFields().isEmpty();
}
private boolean typeIsAnnotated() {
return getTestClass().getJavaClass().isAnnotationPresent(
GeoWaveTestStore.class);
}
protected void setup()
throws Exception {
synchronized (MUTEX) {
TimeZone.setDefault(TimeZone.getTimeZone("GMT"));
for (final TestEnvironment e : testEnvs) {
e.setup();
}
}
}
protected void tearDown()
throws Exception {
synchronized (MUTEX) {
if (!DEFER_CLEANUP.get()) {
// Tearodwn in reverse
final List<TestEnvironment> envs = Arrays.asList(testEnvs);
final ListIterator<TestEnvironment> it = envs.listIterator(envs.size());
while (it.hasPrevious()) {
it.previous().tearDown();
}
}
}
}
private static class GeoWaveStoreRunnerConfig
{
private final Map<String, GeoWaveStoreType> fieldNameStoreTypePair;
public GeoWaveStoreRunnerConfig() {
fieldNameStoreTypePair = new HashMap<String, GeoWaveStoreType>();
}
public GeoWaveStoreRunnerConfig(
final GeoWaveStoreType storeType,
final Set<String> fieldNames ) {
fieldNameStoreTypePair = new HashMap<String, GeoWaveStoreType>();
for (final String fieldName : fieldNames) {
fieldNameStoreTypePair.put(
fieldName,
storeType);
}
}
public GeoWaveStoreRunnerConfig(
final GeoWaveStoreRunnerConfig previousConfig,
final String name,
final GeoWaveStoreType type ) {
if ((previousConfig == null) || (previousConfig.fieldNameStoreTypePair == null)) {
fieldNameStoreTypePair = new HashMap<String, GeoWaveStoreType>();
}
else {
fieldNameStoreTypePair = new HashMap<String, GeoWaveStoreType>(
previousConfig.fieldNameStoreTypePair);
}
fieldNameStoreTypePair.put(
name,
type);
}
}
private static class GeoWaveITException extends
Exception
{
/**
*
*/
private static final long serialVersionUID = 1L;
public GeoWaveITException(
final String message ) {
super(
message);
}
}
private static class TestEnvironmentDependencyTree
{
// just keep a two-way mapping although I think we only need to traverse
// in one direction
Map<TestEnvironment, Set<TestEnvironment>> dependenciesMapping = new LinkedHashMap<TestEnvironment, Set<TestEnvironment>>();
Map<TestEnvironment, Set<TestEnvironment>> requirementsMapping = new LinkedHashMap<TestEnvironment, Set<TestEnvironment>>();
Set<TestEnvironment> independentEnvironments = new LinkedHashSet<TestEnvironment>();
Set<TestEnvironment> visitedEnvs = new LinkedHashSet<TestEnvironment>();
private TestEnvironmentDependencyTree() {}
private void processDependencies(
final TestEnvironment env ) {
if (!visitedEnvs.contains(env)) {
visitedEnvs.add(env);
if ((env.getDependentEnvironments() == null) || (env.getDependentEnvironments().length == 0)) {
independentEnvironments.add(env);
}
else {
for (final TestEnvironment requiredEnv : env.getDependentEnvironments()) {
Set<TestEnvironment> dependentSet = dependenciesMapping.get(requiredEnv);
if (dependentSet == null) {
dependentSet = new HashSet<TestEnvironment>();
dependenciesMapping.put(
requiredEnv,
dependentSet);
}
dependentSet.add(env);
Set<TestEnvironment> requiredSet = requirementsMapping.get(env);
if (requiredSet == null) {
requiredSet = new HashSet<TestEnvironment>();
requirementsMapping.put(
env,
requiredSet);
}
requiredSet.add(requiredEnv);
processDependencies(requiredEnv);
}
}
}
}
private TestEnvironment[] getOrderedTestEnvironments() {
final TestEnvironment[] retVal = new TestEnvironment[visitedEnvs.size()];
int i = 0;
final Set<TestEnvironment> testsAddedToArray = new HashSet<TestEnvironment>();
for (final TestEnvironment e : independentEnvironments) {
retVal[i++] = e;
testsAddedToArray.add(e);
}
for (final TestEnvironment entry : requirementsMapping.keySet()) {
traverseRequirements(
entry,
retVal,
i++,
testsAddedToArray);
}
return retVal;
}
private int traverseRequirements(
final TestEnvironment env,
final TestEnvironment[] currentOrderedArray,
final int startIndex,
final Set<TestEnvironment> testsAddedToArray ) {
int count = 0;
final Set<TestEnvironment> requirements = requirementsMapping.get(env);
for (final TestEnvironment req : requirements) {
if (!testsAddedToArray.contains(req)) {
count = traverseRequirements(
req,
currentOrderedArray,
startIndex + count,
testsAddedToArray);
}
}
currentOrderedArray[startIndex + count++] = env;
testsAddedToArray.add(env);
return count;
}
}
}