blob: ab072215ae6b7804d01645335fa116a546a3135c [file] [log] [blame]
package com.xtremelabs.robolectric;
import android.app.Application;
import com.xtremelabs.robolectric.annotation.DisableStrictI18n;
import com.xtremelabs.robolectric.annotation.EnableStrictI18n;
import com.xtremelabs.robolectric.annotation.Values;
import com.xtremelabs.robolectric.bytecode.ClassHandler;
import com.xtremelabs.robolectric.bytecode.RobolectricClassLoader;
import com.xtremelabs.robolectric.internal.RobolectricTestRunnerInterface;
import com.xtremelabs.robolectric.res.ResourceLoader;
import com.xtremelabs.robolectric.res.ResourcePath;
import com.xtremelabs.robolectric.shadows.ShadowApplication;
import com.xtremelabs.robolectric.shadows.ShadowLog;
import com.xtremelabs.robolectric.shadows.ShadowResources;
import com.xtremelabs.robolectric.util.DatabaseConfig;
import com.xtremelabs.robolectric.util.DatabaseConfig.DatabaseMap;
import com.xtremelabs.robolectric.util.DatabaseConfig.UsingDatabaseMap;
import com.xtremelabs.robolectric.util.SQLiteMap;
import org.junit.runners.BlockJUnit4ClassRunner;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.Statement;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Installs a {@link RobolectricClassLoader} and {@link com.xtremelabs.robolectric.res.ResourceLoader} in order to
* provide a simulation of the Android runtime environment.
*/
public class RobolectricTestRunner extends BlockJUnit4ClassRunner implements RobolectricTestRunnerInterface {
private static Map<AndroidManifest, ResourceLoader> resourceLoaderForRootAndDirectory = new HashMap<AndroidManifest, ResourceLoader>();
// field in both the instrumented and original classes
RobolectricContext sharedRobolectricContext;
// fields in the RobolectricTestRunner in the original ClassLoader
private RobolectricTestRunnerInterface delegate;
private final DatabaseMap databaseMap;
/**
* Creates a runner to run {@code testClass}. Looks in your working directory for your AndroidManifest.xml file
* and res directory.
*
* @param testClass the test class to be run
* @throws InitializationError if junit says so
*/
public RobolectricTestRunner(final Class<?> testClass) throws InitializationError {
super(RobolectricContext.bootstrap(RobolectricTestRunner.class, testClass, new RobolectricContext.Factory() {
@Override
public RobolectricContext create() {
return new RobolectricContext();
}
}));
sharedRobolectricContext = RobolectricContext.mostRecentRobolectricContext; // ick, race condition
if (isBootstrapped(getClass())) {
databaseMap = setupDatabaseMap(testClass, new SQLiteMap());
} else {
delegate = sharedRobolectricContext.getBootstrappedTestRunner(this);
Thread.currentThread().setContextClassLoader(sharedRobolectricContext.getRobolectricClassLoader());
databaseMap = null;
}
}
public RobolectricContext getRobolectricContext() {
return sharedRobolectricContext;
}
protected static boolean isBootstrapped(Class<?> clazz) {
return clazz.getClassLoader() instanceof RobolectricClassLoader;
}
@Override protected Statement methodBlock(final FrameworkMethod method) {
sharedRobolectricContext.getClassHandler().reset();
try {
delegate.internalBeforeTest(method.getMethod());
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
final Statement statement = super.methodBlock(method);
return new Statement() {
@Override public void evaluate() throws Throwable {
HashMap<Field,Object> withConstantAnnos = getWithConstantAnnotations(method.getMethod());
// todo: this try/finally probably isn't right -- should mimic RunAfters? [xw]
try {
if (withConstantAnnos.isEmpty()) {
statement.evaluate();
}
else {
synchronized(this) {
setupConstants(withConstantAnnos);
statement.evaluate();
setupConstants(withConstantAnnos);
}
}
} finally {
delegate.internalAfterTest(method.getMethod());
}
}
};
}
/*
* Called before each test method is run. Sets up the simulation of the Android runtime environment.
*/
@Override final public void internalBeforeTest(final Method method) {
setupLogging();
configureShadows(method);
Robolectric.resetStaticState();
resetStaticState();
DatabaseConfig.setDatabaseMap(databaseMap); //Set static DatabaseMap in DBConfig
setupApplicationState(method);
beforeTest(method);
}
@Override public void internalAfterTest(final Method method) {
afterTest(method);
}
/**
* Called before each test method is run.
*
* @param method the test method about to be run
*/
public void beforeTest(final Method method) {
}
/**
* Called after each test method is run.
*
* @param method the test method that just ran.
*/
public void afterTest(final Method method) {
}
/**
* You probably don't want to override this method. Override #prepareTest(Object) instead.
*
* @see BlockJUnit4ClassRunner#createTest()
*/
@Override
public Object createTest() throws Exception {
if (delegate != null) {
return delegate.createTest();
} else {
Object test = super.createTest();
prepareTest(test);
return test;
}
}
public void prepareTest(final Object test) {
}
public void setupApplicationState(Method testMethod) {
boolean strictI18n = determineI18nStrictState(testMethod);
ResourceLoader systemResourceLoader = new ResourceLoader(sharedRobolectricContext.getSystemResourcePath());
ShadowResources.setSystemResources(systemResourceLoader);
ResourceLoader resourceLoader = getResourceLoader(sharedRobolectricContext.getAppManifest());
resourceLoader.setQualifiers(determineResourceQualifiers(testMethod));
resourceLoader.setStrictI18n(strictI18n);
ClassHandler classHandler = sharedRobolectricContext.getClassHandler();
classHandler.setStrictI18n(strictI18n);
Robolectric.application = ShadowApplication.bind(createApplication(), resourceLoader);
}
protected void configureShadows(Method testMethod) { // todo: dedupe this/bindShadowClasses
Robolectric.bindDefaultShadowClasses();
bindShadowClasses(testMethod);
}
/**
* Override this method to bind your own shadow classes
*/
@SuppressWarnings("UnusedParameters")
protected void bindShadowClasses(Method testMethod) {
bindShadowClasses();
}
/**
* Override this method to bind your own shadow classes
*/
protected void bindShadowClasses() {
}
/**
* Override this method to reset the state of static members before each test.
*/
protected void resetStaticState() {
}
private String determineResourceQualifiers(Method method) {
String qualifiers = "";
Values values = method.getAnnotation(Values.class);
if (values != null) {
qualifiers = values.qualifiers();
if (qualifiers.isEmpty()) {
qualifiers = values.locale();
}
}
return qualifiers;
}
/**
* Sets Robolectric config to determine if Robolectric should blacklist API calls that are not
* I18N/L10N-safe.
* <p/>
* I18n-strict mode affects suitably annotated shadow methods. Robolectric will throw exceptions
* if these methods are invoked by application code. Additionally, Robolectric's ResourceLoader
* will throw exceptions if layout resources use bare string literals instead of string resource IDs.
* <p/>
* To enable or disable i18n-strict mode for specific test cases, annotate them with
* {@link com.xtremelabs.robolectric.annotation.EnableStrictI18n} or
* {@link com.xtremelabs.robolectric.annotation.DisableStrictI18n}.
* <p/>
*
* By default, I18n-strict mode is disabled.
*
* @param method
*
*/
private boolean determineI18nStrictState(Method method) {
// Global
boolean strictI18n = globalI18nStrictEnabled();
// Test case class
Class<?> testClass = method.getDeclaringClass();
if (testClass.getAnnotation(EnableStrictI18n.class) != null) {
strictI18n = true;
} else if (testClass.getAnnotation(DisableStrictI18n.class) != null) {
strictI18n = false;
}
// Test case method
if (method.getAnnotation(EnableStrictI18n.class) != null) {
strictI18n = true;
} else if (method.getAnnotation(DisableStrictI18n.class) != null) {
strictI18n = false;
}
return strictI18n;
}
/**
* Default implementation of global switch for i18n-strict mode.
* To enable i18n-strict mode globally, set the system property
* "robolectric.strictI18n" to true. This can be done via java
* system properties in either Ant or Maven.
* <p/>
* Subclasses can override this method and establish their own policy
* for enabling i18n-strict mode.
*
* @return
*/
protected boolean globalI18nStrictEnabled() {
return Boolean.valueOf(System.getProperty("robolectric.strictI18n"));
}
/**
* Find all the class and method annotations and pass them to
* addConstantFromAnnotation() for evaluation.
*
* TODO: Add compound annotations to suport defining more than one int and string at a time
* TODO: See http://stackoverflow.com/questions/1554112/multiple-annotations-of-the-same-type-on-one-element
*
* @param method
* @return
*/
private HashMap<Field,Object> getWithConstantAnnotations(Method method) {
HashMap<Field,Object> constants = new HashMap<Field,Object>();
for(Annotation anno:method.getDeclaringClass().getAnnotations()) {
addConstantFromAnnotation(constants, anno);
}
for(Annotation anno:method.getAnnotations()) {
addConstantFromAnnotation(constants, anno);
}
return constants;
}
/**
* If the annotation is a constant redefinition, add it to the provided hash
*
* @param constants
* @param anno
*/
private void addConstantFromAnnotation(HashMap<Field,Object> constants, Annotation anno) {
try {
String name = anno.annotationType().getName();
Object newValue = null;
if (name.equals( "com.xtremelabs.robolectric.annotation.WithConstantString" )) {
newValue = (String) anno.annotationType().getMethod("newValue").invoke(anno);
}
else if (name.equals( "com.xtremelabs.robolectric.annotation.WithConstantInt" )) {
newValue = (Integer) anno.annotationType().getMethod("newValue").invoke(anno);
}
else {
return;
}
@SuppressWarnings("rawtypes")
Class classWithField = (Class) anno.annotationType().getMethod("classWithField").invoke(anno);
String fieldName = (String) anno.annotationType().getMethod("fieldName").invoke(anno);
Field field = classWithField.getDeclaredField(fieldName);
constants.put(field, newValue);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* Defines static finals from the provided hash and stores the old values back
* into the hash.
*
* Call it twice with the same hash, and it puts everything back the way it was originally.
*
* @param constants
*/
private void setupConstants(HashMap<Field,Object> constants) {
for(Field field:constants.keySet()) {
Object newValue = constants.get(field);
Object oldValue = Robolectric.Reflection.setFinalStaticField(field, newValue);
constants.put(field,oldValue);
}
}
private void setupLogging() {
String logging = System.getProperty("robolectric.logging");
if (logging != null && ShadowLog.stream == null) {
PrintStream stream = null;
if ("stdout".equalsIgnoreCase(logging)) {
stream = System.out;
} else if ("stderr".equalsIgnoreCase(logging)) {
stream = System.err;
} else {
try {
final PrintStream file = new PrintStream(new FileOutputStream(logging));
stream = file;
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override public void run() {
try { file.close(); } catch (Exception ignored) { }
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
ShadowLog.stream = stream;
}
}
/**
* Override this method if you want to provide your own implementation of Application.
* <p/>
* This method attempts to instantiate an application instance as specified by the AndroidManifest.xml.
*
* @return An instance of the Application class specified by the ApplicationManifest.xml or an instance of
* Application if not specified.
*/
protected Application createApplication() {
return new ApplicationResolver(sharedRobolectricContext.getAppManifest()).resolveApplication();
}
private ResourceLoader getResourceLoader(final AndroidManifest androidManifest) {
ResourceLoader resourceLoader = resourceLoaderForRootAndDirectory.get(androidManifest);
if (resourceLoader == null ) {
List<ResourcePath> resourcePaths = sharedRobolectricContext.getResourcePaths();
resourceLoader = createResourceLoader(resourcePaths);
resourceLoaderForRootAndDirectory.put(androidManifest, resourceLoader);
}
return resourceLoader;
}
// this method must live on a RobolectricClassLoader-loaded class, so it can't be on RobolectricContext
protected ResourceLoader createResourceLoader(List<ResourcePath> resourcePaths) {
return new ResourceLoader(resourcePaths);
}
private String findResourcePackageName(final File projectManifestFile) throws ParserConfigurationException, IOException, SAXException {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
Document doc = db.parse(projectManifestFile);
String projectPackage = doc.getElementsByTagName("manifest").item(0).getAttributes().getNamedItem("package").getTextContent();
return projectPackage + ".R";
}
/*
* Specifies what database to use for testing (ex: H2 or Sqlite),
* this will load H2 by default, the SQLite TestRunner version will override this.
*/
protected DatabaseMap setupDatabaseMap(Class<?> testClass, DatabaseMap map) {
DatabaseMap dbMap = map;
if (testClass.isAnnotationPresent(UsingDatabaseMap.class)) {
UsingDatabaseMap usingMap = testClass.getAnnotation(UsingDatabaseMap.class);
if(usingMap.value()!=null){
dbMap = Robolectric.newInstanceOf(usingMap.value());
} else {
if (dbMap==null)
throw new RuntimeException("UsingDatabaseMap annotation value must provide a class implementing DatabaseMap");
}
}
return dbMap;
}
}