blob: 50fc33bf57e45de52cfd2eb954e2c63e3df3b92d [file] [log] [blame]
package com.xtremelabs.robolectric;
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.Constructor;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Logger;
import javassist.Loader;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
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 android.app.Application;
import android.net.Uri__FromAndroid;
import com.xtremelabs.robolectric.bytecode.ClassHandler;
import com.xtremelabs.robolectric.bytecode.RobolectricClassLoader;
import com.xtremelabs.robolectric.bytecode.ShadowWrangler;
import com.xtremelabs.robolectric.internal.RealObject;
import com.xtremelabs.robolectric.internal.RobolectricTestRunnerInterface;
import com.xtremelabs.robolectric.res.ResourceLoader;
import com.xtremelabs.robolectric.shadows.ShadowApplication;
import com.xtremelabs.robolectric.shadows.ShadowLog;
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;
/**
* 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 final String MANIFEST_PATH_PROPERTY = "robolectric.path.manifest";
private static final String RES_PATH_PROPERTY = "robolectric.path.res";
private static final String ASSETS_PATH_PROPERTY = "robolectric.path.assets";
private static final String DEFAULT_MANIFEST_PATH = "./AndroidManifest.xml";
private static final String DEFAULT_RES_PATH = "./res";
private static final String DEFAULT_ASSETS_PATH = "./assets";
private static final Logger logger =
Logger.getLogger(RobolectricTestRunner.class.getSimpleName());
/** Instrument detector. We use it to check whether the current instance is instrumented. */
private static InstrumentDetector instrumentDetector = InstrumentDetector.DEFAULT;
private static RobolectricClassLoader defaultLoader;
private static Map<RobolectricConfig, ResourceLoader> resourceLoaderForRootAndDirectory = new HashMap<RobolectricConfig, ResourceLoader>();
// fields in the RobolectricTestRunner in the original ClassLoader
private RobolectricClassLoader classLoader;
private ClassHandler classHandler;
private RobolectricTestRunnerInterface delegate;
private DatabaseMap databaseMap;
// fields in the RobolectricTestRunner in the instrumented ClassLoader
protected RobolectricConfig robolectricConfig;
private static RobolectricClassLoader getDefaultLoader() {
if (defaultLoader == null) {
defaultLoader = new RobolectricClassLoader(ShadowWrangler.getInstance());
}
return defaultLoader;
}
public static void setInstrumentDetector(final InstrumentDetector detector) {
instrumentDetector = detector;
}
public static void setDefaultLoader(Loader robolectricClassLoader) {
//used by the RoboSpecs project to allow for mixed scala\java tests to be run with Maven Surefire (see the RoboSpecs project on github)
if (defaultLoader == null) {
defaultLoader = (RobolectricClassLoader)robolectricClassLoader;
} else throw new RuntimeException("You may not set the default robolectricClassLoader unless it is null!");
}
/**
* Call this if you would like Robolectric to rewrite additional classes and turn them
* into "do nothing" classes which proxy all method calls to shadow classes, just like it does
* with the android classes by default.
*
* @param classOrPackageToBeInstrumented fully-qualified class or package name
*/
protected static void addClassOrPackageToInstrument(String classOrPackageToBeInstrumented) {
if (!isInstrumented()) {
defaultLoader.addCustomShadowClass(classOrPackageToBeInstrumented);
}
}
/**
* 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 {
this(testClass, new RobolectricConfig(
new File(getSystemProperty(MANIFEST_PATH_PROPERTY, DEFAULT_MANIFEST_PATH)),
new File(getSystemProperty(RES_PATH_PROPERTY, DEFAULT_RES_PATH)),
new File(getSystemProperty(ASSETS_PATH_PROPERTY, DEFAULT_ASSETS_PATH))));
}
/**
* 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
* @param classLoader a custom RobolectricClassLoader to be used.
* @throws InitializationError if junit says so
*/
public RobolectricTestRunner(final Class<?> testClass, RobolectricClassLoader classLoader)
throws InitializationError {
this(testClass,
isInstrumented() ? null : ShadowWrangler.getInstance(),
isInstrumented() ? null : classLoader,
new RobolectricConfig(
new File(getSystemProperty(MANIFEST_PATH_PROPERTY, DEFAULT_MANIFEST_PATH)),
new File(getSystemProperty(RES_PATH_PROPERTY, DEFAULT_RES_PATH)),
new File(getSystemProperty(ASSETS_PATH_PROPERTY, DEFAULT_ASSETS_PATH))));
}
/**
* Call this constructor in subclasses in order to specify non-default configuration (e.g. location of the
* AndroidManifest.xml file and resource directory).
*
* @param testClass the test class to be run
* @param robolectricConfig the configuration data
* @throws InitializationError if junit says so
*/
protected RobolectricTestRunner(final Class<?> testClass, final RobolectricConfig robolectricConfig)
throws InitializationError {
this(testClass,
isInstrumented() ? null : ShadowWrangler.getInstance(),
isInstrumented() ? null : getDefaultLoader(),
robolectricConfig, new SQLiteMap());
}
/**
* Call this constructor in subclasses in order to specify non-default configuration (e.g. location of the
* AndroidManifest.xml file, resource directory, and DatabaseMap).
*
* @param testClass the test class to be run
* @param robolectricConfig the configuration data
* @param databaseMap the database mapping
* @throws InitializationError if junit says so
*/
protected RobolectricTestRunner(Class<?> testClass, RobolectricConfig robolectricConfig, DatabaseMap databaseMap)
throws InitializationError {
this(testClass,
isInstrumented() ? null : ShadowWrangler.getInstance(),
isInstrumented() ? null : getDefaultLoader(),
robolectricConfig, databaseMap);
}
/**
* Call this constructor in subclasses in order to specify the project root directory.
*
* @param testClass the test class to be run
* @param androidProjectRoot the directory containing your AndroidManifest.xml file and res dir
* @throws InitializationError if the test class is malformed
*/
public RobolectricTestRunner(final Class<?> testClass, final File androidProjectRoot) throws InitializationError {
this(testClass, new RobolectricConfig(androidProjectRoot));
}
/**
* Call this constructor in subclasses in order to specify the project root directory.
*
* @param testClass the test class to be run
* @param androidProjectRoot the directory containing your AndroidManifest.xml file and res dir
* @throws InitializationError if junit says so
* @deprecated Use {@link #RobolectricTestRunner(Class, File)} instead.
*/
@Deprecated
public RobolectricTestRunner(final Class<?> testClass, final String androidProjectRoot) throws InitializationError {
this(testClass, new RobolectricConfig(new File(androidProjectRoot)));
}
/**
* Call this constructor in subclasses in order to specify the location of the AndroidManifest.xml file and the
* resource directory. The #androidManifestPath is used to locate the AndroidManifest.xml file which, in turn,
* contains package name for the {@code R} class which contains the identifiers for all of the resources. The
* resource directory is where the resource loader will look for resources to load.
*
* @param testClass the test class to be run
* @param androidManifestPath the AndroidManifest.xml file
* @param resourceDirectory the directory containing the project's resources
* @throws InitializationError if junit says so
*/
protected RobolectricTestRunner(final Class<?> testClass, final File androidManifestPath, final File resourceDirectory)
throws InitializationError {
this(testClass, new RobolectricConfig(androidManifestPath, resourceDirectory));
}
/**
* Call this constructor in subclasses in order to specify the location of the AndroidManifest.xml file and the
* resource directory. The #androidManifestPath is used to locate the AndroidManifest.xml file which, in turn,
* contains package name for the {@code R} class which contains the identifiers for all of the resources. The
* resource directory is where the resource loader will look for resources to load.
*
* @param testClass the test class to be run
* @param androidManifestPath the relative path to the AndroidManifest.xml file
* @param resourceDirectory the relative path to the directory containing the project's resources
* @throws InitializationError if junit says so
* @deprecated Use {@link #RobolectricTestRunner(Class, File, File)} instead.
*/
@Deprecated
protected RobolectricTestRunner(final Class<?> testClass, final String androidManifestPath, final String resourceDirectory)
throws InitializationError {
this(testClass, new RobolectricConfig(new File(androidManifestPath), new File(resourceDirectory)));
}
protected RobolectricTestRunner(Class<?> testClass, ClassHandler classHandler, RobolectricClassLoader classLoader, RobolectricConfig robolectricConfig) throws InitializationError {
this(testClass, classHandler, classLoader, robolectricConfig, new SQLiteMap());
}
/**
* This is not the constructor you are looking for... probably. This constructor creates a bridge between the test
* runner called by JUnit and a second instance of the test runner that is loaded via the instrumenting class
* loader. This instrumented instance of the test runner, along with the instrumented instance of the actual test,
* provides access to Robolectric's features and the un-instrumented instance of the test runner delegates most of
* the interesting test runner behavior to it. Providing your own class handler and class loader here in order to
* get different functionality is a difficult and dangerous project. If you need to customize the project root and
* resource directory, use {@link #RobolectricTestRunner(Class, String, String)}. For other extensions, consider
* creating a subclass and overriding the documented methods of this class.
*
* @param testClass the test class to be run
* @param classHandler the {@link ClassHandler} to use to in shadow delegation
* @param classLoader the {@link RobolectricClassLoader}
* @param robolectricConfig the configuration
* @throws InitializationError if junit says so
*/
protected RobolectricTestRunner(final Class<?> testClass, final ClassHandler classHandler, final RobolectricClassLoader classLoader, final RobolectricConfig robolectricConfig, final DatabaseMap map) throws InitializationError {
super(isInstrumented() ? testClass
: ensureClassLoaderNotNull(classLoader).bootstrap(testClass));
if (!isInstrumented()) {
this.classHandler = classHandler;
this.classLoader = ensureClassLoaderNotNull(classLoader);
this.robolectricConfig = robolectricConfig;
this.databaseMap = setupDatabaseMap(testClass, map);
Thread.currentThread().setContextClassLoader(classLoader);
delegateLoadingOf(Uri__FromAndroid.class.getName());
delegateLoadingOf(RobolectricTestRunnerInterface.class.getName());
delegateLoadingOf(RealObject.class.getName());
delegateLoadingOf(ShadowWrangler.class.getName());
delegateLoadingOf(RobolectricConfig.class.getName());
delegateLoadingOf(DatabaseMap.class.getName());
delegateLoadingOf(android.R.class.getName());
Class<?> delegateClass = classLoader.bootstrap(this.getClass());
try {
Constructor<?> constructorForDelegate = delegateClass.getConstructor(Class.class);
this.delegate = (RobolectricTestRunnerInterface) constructorForDelegate.newInstance(classLoader.bootstrap(testClass));
this.delegate.setRobolectricConfig(robolectricConfig);
this.delegate.setDatabaseMap(databaseMap);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
private static RobolectricClassLoader ensureClassLoaderNotNull(
RobolectricClassLoader classLoader) {
return classLoader == null ? getDefaultLoader() : classLoader;
}
protected static boolean isInstrumented() {
return instrumentDetector.isInstrumented();
}
/**
* Only used when creating the delegate instance within the instrumented ClassLoader.
* <p/>
* This is not the constructor you are looking for.
*/
@SuppressWarnings({"UnusedDeclaration", "JavaDoc"})
protected RobolectricTestRunner(final Class<?> testClass, final ClassHandler classHandler, final RobolectricConfig robolectricConfig) throws InitializationError {
super(testClass);
this.classHandler = classHandler;
this.robolectricConfig = robolectricConfig;
}
/** @deprecated use {@link Robolectric.Reflection#setFinalStaticField(Class, String, Object)} */
@Deprecated
public static void setStaticValue(Class<?> clazz, String fieldName, Object value) {
Robolectric.Reflection.setFinalStaticField(clazz, fieldName, value);
}
protected void delegateLoadingOf(final String className) {
classLoader.delegateLoadingOf(className);
}
@Override protected Statement methodBlock(final FrameworkMethod method) {
setupI18nStrictState(method.getMethod(), robolectricConfig);
lookForLocaleAnnotation( method.getMethod(), robolectricConfig );
if (classHandler != null) {
classHandler.configure(robolectricConfig);
classHandler.beforeTest();
}
delegate.internalBeforeTest(method.getMethod());
final Statement statement = super.methodBlock(method);
return new Statement() {
@Override public void evaluate() throws Throwable {
// todo: this try/finally probably isn't right -- should mimic RunAfters? [xw]
try {
statement.evaluate();
} finally {
delegate.internalAfterTest(method.getMethod());
if (classHandler != null) {
classHandler.afterTest();
}
}
}
};
}
/*
* Called before each test method is run. Sets up the simulation of the Android runtime environment.
*/
@Override public void internalBeforeTest(final Method method) {
setupApplicationState(robolectricConfig);
beforeTest(method);
}
@Override public void internalAfterTest(final Method method) {
afterTest(method);
}
@Override public void setRobolectricConfig(final RobolectricConfig robolectricConfig) {
this.robolectricConfig = robolectricConfig;
}
/**
* 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(final RobolectricConfig robolectricConfig) {
setupLogging();
ResourceLoader resourceLoader = createResourceLoader(robolectricConfig );
Robolectric.bindDefaultShadowClasses();
bindShadowClasses();
resourceLoader.setLayoutQualifierSearchPath();
Robolectric.resetStaticState();
resetStaticState();
DatabaseConfig.setDatabaseMap(this.databaseMap);//Set static DatabaseMap in DBConfig
Robolectric.application = ShadowApplication.bind(createApplication(), resourceLoader);
}
/**
* 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 static String getSystemProperty(String propertyName, String defaultValue) {
String property = System.getProperty(propertyName);
if (property == null) {
property = defaultValue;
logger.info("No system property " + propertyName + " found, default to "
+ defaultValue);
}
return property;
}
/**
* 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
* @param robolectricConfig
*/
private void setupI18nStrictState(Method method, RobolectricConfig robolectricConfig) {
// Global
boolean strictI18n = globalI18nStrictEnabled();
// Test case class
Annotation[] annos = method.getDeclaringClass().getAnnotations();
strictI18n = lookForI18nAnnotations(strictI18n, annos);
// Test case methods
annos = method.getAnnotations();
strictI18n = lookForI18nAnnotations(strictI18n, annos);
robolectricConfig.setStrictI18n(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"));
}
/**
* As test methods are loaded by the delegate's class loader, the normal
* method#isAnnotationPresent test fails. Look at string versions of the
* annotation names to test for their presence.
*
* @param strictI18n
* @param annos
* @return
*/
private boolean lookForI18nAnnotations(boolean strictI18n, Annotation[] annos) {
for ( int i = 0; i < annos.length; i++ ) {
String name = annos[i].annotationType().getName();
if (name.equals("com.xtremelabs.robolectric.annotation.EnableStrictI18n")) {
strictI18n = true;
break;
}
if (name.equals("com.xtremelabs.robolectric.annotation.DisableStrictI18n")) {
strictI18n = false;
break;
}
}
return strictI18n;
}
private void lookForLocaleAnnotation( Method method, RobolectricConfig robolectricConfig ){
String locale = "";
// TODO: there are maybe better implementation for getAnnotation
// Have tried to use several other simple ways, but failed.
Annotation[] annos = method.getDeclaredAnnotations();
for( Annotation anno: annos ){
if( anno.annotationType().getName().equals( "com.xtremelabs.robolectric.annotation.Values" )){
String annotationString = anno.toString();
int startIndex = annotationString.indexOf( '=' );
int endIndex = annotationString.indexOf( ')' );
if( startIndex < 0 || endIndex < 0 ){ return; }
locale = annotationString.substring( startIndex + 1, endIndex );
}
}
robolectricConfig.setLocale( locale );
}
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(robolectricConfig).resolveApplication();
}
private ResourceLoader createResourceLoader(final RobolectricConfig robolectricConfig) {
ResourceLoader resourceLoader = resourceLoaderForRootAndDirectory.get(robolectricConfig);
// When locale has changed, reload the resource files.
if (resourceLoader == null || robolectricConfig.isLocaleChanged() ) {
try {
robolectricConfig.validate();
String rClassName = robolectricConfig.getRClassName();
Class rClass;
try {
rClass = Class.forName(rClassName);
} catch (ClassNotFoundException e) {
rClass = null;
}
resourceLoader = new ResourceLoader(robolectricConfig.getRealSdkVersion(), rClass, robolectricConfig.getResourceDirectory(), robolectricConfig.getAssetsDirectory(), robolectricConfig.getLocale() );
resourceLoaderForRootAndDirectory.put(robolectricConfig, resourceLoader);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
resourceLoader.setStrictI18n(robolectricConfig.getStrictI18n());
return resourceLoader;
}
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;
}
public DatabaseMap getDatabaseMap() {
return databaseMap;
}
@Override
public void setDatabaseMap(DatabaseMap databaseMap) {
this.databaseMap = databaseMap;
}
/**
* Detects whether current instance is already instrumented.
*/
public interface InstrumentDetector {
/** Default detector. */
InstrumentDetector DEFAULT = new InstrumentDetector() {
@Override
public boolean isInstrumented() {
return RobolectricTestRunner.class.getClassLoader().getClass().getName().contains(RobolectricClassLoader.class.getName());
}
};
/**
* @return true if current instance is already instrumented
*/
boolean isInstrumented();
}
}