blob: 426337f8028fa616f6ca7caeb816c52e9078785a [file] [log] [blame]
/*
* Copyright (c) 2010 Artem Tikhomirov 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:
* Artem Tikhomirov (independent) - initial API and implementation
*/
package org.eclipse.gmf.tests;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import org.junit.Assert;
import junit.framework.TestCase;
import junit.framework.TestSuite;
/**
* The idea is to minimize number of static fields throughout tests;
* to keep tests configuration externally to tests themselves;
* to allow reuse of single setup instance in partial rerun of few tests
* @author artem
*/
public class Configurator {
private final Map<Class<? extends TestCase>, Class<? extends TestConfiguration>> config;
private final Map<Class<? extends TestConfiguration>, TestConfiguration> state;
// tests from single TestCase usually run sequentially, and never mix with other
// TestCases, hence enough to cache only one class/set of methods
private Class<?> lastSetupLookupTarget;
private Method[] lastSetupLookupResult;
public Configurator() {
config = new HashMap<Class<? extends TestCase>, Class<? extends TestConfiguration>>();
state = new HashMap<Class<? extends TestConfiguration>, TestConfiguration>();
}
public void dispose() {
config.clear();
state.clear();
}
// configuration instance would get created on demand
public void register(Class<? extends TestCase> target, Class<? extends TestConfiguration> configClass) {
config.put(target, configClass);
}
// supplied configuration instance would be used. useful when instance initialization takes extra steps
// and/or should be done in advance (i.e. before running any test)
public void register(Class<? extends TestCase> target, TestConfiguration configInstance) {
Class<? extends TestConfiguration> configClass = configInstance.getClass();
register(target, configClass);
registerInstance(configClass, configInstance);
}
//
public void registerInstance(Class<? extends TestConfiguration> configClass, TestConfiguration configInstance) {
state.put(configClass, configInstance);
}
// potentially may provide different configuration per separate test, although we don't use this now
private TestConfiguration getConfig(Class<? extends TestCase> target, String testName, boolean instantiate) {
Class<? extends TestConfiguration> configClass = config.get(target);
if (configClass == null) {
return null;
}
TestConfiguration configInstance = state.get(configClass);
if (configInstance == null && instantiate) {
configInstance = newInstance(configClass);
state.put(configClass, configInstance); // may be null
}
return configInstance;
}
private TestConfiguration newInstance(Class<? extends TestConfiguration> configClass) {
// in fact, getMethods may lead to troubles if we have subclasses of TestConfiguration with
// few newInstance methods, and we pick the one from superclass first. To fix, need to iterate over
// subclasses manually, but too lazy now.
for (Method m : configClass.getMethods()) {
if (Modifier.isStatic(m.getModifiers()) && m.getAnnotation(TestConfiguration.FactoryMethod.class) != null) {
if (configClass.isAssignableFrom(m.getReturnType())) {
try {
// null is ok, client's troubles then
return (TestConfiguration) m.invoke(null);
} catch (Exception ex) {
ex.printStackTrace();
// FALL-THROUGH - fail
}
}
Assert.fail("Bad factory method: " + m);
assert false; // should not ever get here
}
}
Assert.fail("Can't find factory method in " + configClass);
return null;
}
// instance method just to allow caching
private Method[] getSetupMethods(Class<?> target) {
if (lastSetupLookupTarget == target) {
return lastSetupLookupResult;
}
lastSetupLookupTarget = target;
lastSetupLookupResult = findSetupMethods(target);
return lastSetupLookupResult;
}
private static Method[] findSetupMethods(Class<?> target) {
ArrayList<Method> setupMethods = new ArrayList<Method>(3);
for (Method m : target.getMethods()) {
// may use annotation arguments to denote different configure methods (e.g. taking more than one argument)
if (m.getAnnotation(NeedsSetup.class) != null) {
Class<?>[] parameterTypes = m.getParameterTypes();
if (parameterTypes.length == 1 && TestConfiguration.class.isAssignableFrom(parameterTypes[0])) {
setupMethods.add(m);
} else {
Assert.fail("Invalid test configuration method:" + m);
}
}
}
return setupMethods.toArray(new Method[setupMethods.size()]);
}
// calls back to initialize test with appropriate configuration object
public void prepare(TestCase test) {
Class<? extends TestCase> testClass = test.getClass();
Method[] setupMethods = getSetupMethods(testClass);
if (setupMethods.length == 0) {
// fail? OTOH, test would fail itself if no expected setup would be supplied.
// XXX seems reasonable to fail only in case of programming errors (e.g. incorrectly marked method)
// and do not fail if none matching setups/methods found.
return;
}
TestConfiguration config = getConfig(testClass, test.getName(), true);
if (config == null) {
return;
}
for (Method m : setupMethods) {
// invoke all compatible setup methods, ignore those that not good.
if (m.getParameterTypes()[0].isInstance(config)) {
try {
m.invoke(test, config);
} catch (Exception ex) {
// wrong configuration method IS programming error
Assert.fail(ex.toString());
}
}
}
}
public static TestSuite feed(Class<? extends TestCase> theClass, TestConfiguration config, String suiteName) {
TestSuite suite = new TestSuite(theClass, suiteName);
try {
for (Method m : findSetupMethods(theClass)) {
if (m.getParameterTypes()[0].isInstance(config)) {
for (Enumeration<?> en = suite.tests(); en.hasMoreElements(); ) {
Object nextTest = en.nextElement();
m.invoke(nextTest, config);
}
}
}
} catch (Exception ex) {
suite.addTest(new ConfigurationFailedCase(theClass.getName() + ": " + ex.getMessage(), ex));
}
return suite;
}
}