blob: c47b0ec18a012093b8a61e4a6b8e820c001d5dcf [file] [log] [blame]
package org.robolectric;
import android.app.Application;
import android.os.Build;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.TestOnly;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.internal.AssumptionViolatedException;
import org.junit.internal.runners.model.EachTestNotifier;
import org.junit.runner.Description;
import org.junit.runner.notification.RunNotifier;
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.junit.runners.model.TestClass;
import org.robolectric.annotation.Config;
import org.robolectric.internal.*;
import org.robolectric.internal.bytecode.*;
import org.robolectric.internal.dependency.*;
import org.robolectric.manifest.AndroidManifest;
import org.robolectric.res.*;
import org.robolectric.util.Logger;
import org.robolectric.util.ReflectionHelpers;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.security.SecureRandom;
import java.util.*;
/**
* Installs a {@link org.robolectric.internal.bytecode.InstrumentingClassLoader} and
* {@link ResourceTable} in order to provide a simulation of the Android runtime environment.
*/
public class RobolectricTestRunner extends BlockJUnit4ClassRunner {
public static final String CONFIG_PROPERTIES = "robolectric.properties";
private static final Map<AndroidManifest, PackageResourceTable> appResourceTableCache = new HashMap<>();
private static final Map<ManifestIdentifier, AndroidManifest> appManifestsCache = new HashMap<>();
private static PackageResourceTable compiletimeSdkResourceTable;
private final SdkPicker sdkPicker;
private final ConfigMerger configMerger;
private TestLifecycle<Application> testLifecycle;
private DependencyResolver dependencyResolver;
static {
new SecureRandom(); // this starts up the Poller SunPKCS11-Darwin thread early, outside of any Robolectric classloader
}
private final HashSet<Class<?>> loadedTestClasses = new HashSet<>();
/**
* Creates a runner to run {@code testClass}. Looks in your working directory for your AndroidManifest.xml file
* and res directory by default. Use the {@link Config} annotation to configure.
*
* @param testClass the test class to be run
* @throws InitializationError if junit says so
*/
public RobolectricTestRunner(final Class<?> testClass) throws InitializationError {
super(testClass);
this.configMerger = createConfigMerger();
this.sdkPicker = createSdkPicker();
}
@SuppressWarnings("unchecked")
private void assureTestLifecycle(SdkEnvironment sdkEnvironment) {
try {
ClassLoader robolectricClassLoader = sdkEnvironment.getRobolectricClassLoader();
testLifecycle = (TestLifecycle) robolectricClassLoader.loadClass(getTestLifecycleClass().getName()).newInstance();
} catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
protected DependencyResolver getJarResolver() {
if (dependencyResolver == null) {
if (Boolean.getBoolean("robolectric.offline")) {
String dependencyDir = System.getProperty("robolectric.dependency.dir", ".");
dependencyResolver = new LocalDependencyResolver(new File(dependencyDir));
} else {
File cacheDir = new File(new File(System.getProperty("java.io.tmpdir")), "robolectric");
if (cacheDir.exists() || cacheDir.mkdir()) {
Logger.info("Dependency cache location: %s", cacheDir.getAbsolutePath());
dependencyResolver = new CachedDependencyResolver(new MavenDependencyResolver(), cacheDir, 60 * 60 * 24 * 1000);
} else {
dependencyResolver = new MavenDependencyResolver();
}
}
URL buildPathPropertiesUrl = getClass().getClassLoader().getResource("robolectric-deps.properties");
if (buildPathPropertiesUrl != null) {
Logger.info("Using Robolectric classes from %s", buildPathPropertiesUrl.getPath());
FsFile propertiesFile = Fs.fileFromPath(buildPathPropertiesUrl.getFile());
try {
dependencyResolver = new PropertiesDependencyResolver(propertiesFile, dependencyResolver);
} catch (IOException e) {
throw new RuntimeException("couldn't read " + buildPathPropertiesUrl, e);
}
}
}
return dependencyResolver;
}
/**
* Create a {@link ClassHandler} appropriate for the given arguments.
*
* Robolectric may chose to cache the returned instance, keyed by <tt>shadowMap</tt> and <tt>sdkConfig</tt>.
*
* Custom TestRunner subclasses may wish to override this method to provide alternate configuration.
*
* @param shadowMap the {@link ShadowMap} in effect for this test
* @param sdkConfig the {@link SdkConfig} in effect for this test
* @return an appropriate {@link ClassHandler}. This implementation returns a {@link ShadowWrangler}.
* @since 2.3
*/
@NotNull
protected ClassHandler createClassHandler(ShadowMap shadowMap, SdkConfig sdkConfig) {
return new ShadowWrangler(shadowMap, sdkConfig.getApiLevel());
}
/**
* Create a {@link ConfigMerger} for calculating the {@link Config} tests.
*
* Custom TestRunner subclasses may wish to override this method to provide alternate configuration.
*
* @return an {@link ConfigMerger}.
* @since 3.2
*/
@NotNull
private ConfigMerger createConfigMerger() {
return new ConfigMerger();
}
/**
* Create a {@link SdkPicker} for determining which SDKs will be tested.
*
* Custom TestRunner subclasses may wish to override this method to provide alternate configuration.
*
* @return an {@link SdkPicker}.
* @since 3.2
*/
@NotNull
protected SdkPicker createSdkPicker() {
return new SdkPicker();
}
/**
* Create an {@link InstrumentationConfiguration} suitable for the provided {@link Config}.
*
* Custom TestRunner subclasses may wish to override this method to provide alternate configuration.
*
* @param config the merged configuration for the test that's about to run
* @return an {@link InstrumentationConfiguration}
*/
@NotNull
public InstrumentationConfiguration createClassLoaderConfig(Config config) {
return InstrumentationConfiguration.newBuilder().withConfig(config).build();
}
/**
* An instance of the returned class will be created for each test invocation.
*
* Custom TestRunner subclasses may wish to override this method to provide alternate configuration.
*
* @return a class which implements {@link TestLifecycle}. This implementation returns a {@link DefaultTestLifecycle}.
*/
@NotNull
protected Class<? extends TestLifecycle> getTestLifecycleClass() {
return DefaultTestLifecycle.class;
}
public static void injectEnvironment(ClassLoader robolectricClassLoader,
ClassHandler classHandler, ShadowInvalidator invalidator) {
String className = RobolectricInternals.class.getName();
Class<?> robolectricInternalsClass = ReflectionHelpers.loadClass(robolectricClassLoader, className);
ReflectionHelpers.setStaticField(robolectricInternalsClass, "classHandler", classHandler);
ReflectionHelpers.setStaticField(robolectricInternalsClass, "shadowInvalidator", invalidator);
}
@Override
protected Statement classBlock(RunNotifier notifier) {
final Statement statement = childrenInvoker(notifier);
return new Statement() {
@Override
public void evaluate() throws Throwable {
try {
statement.evaluate();
for (Class<?> testClass : loadedTestClasses) {
invokeAfterClass(testClass);
}
} finally {
afterClass();
loadedTestClasses.clear();
}
}
};
}
@Override
protected List<FrameworkMethod> getChildren() {
List<FrameworkMethod> children = new ArrayList<>();
for (FrameworkMethod frameworkMethod : super.getChildren()) {
Config config = getConfig(frameworkMethod.getMethod());
AndroidManifest appManifest = getAppManifest(config);
List<SdkConfig> sdksToRun = sdkPicker.selectSdks(config, appManifest);
RobolectricFrameworkMethod last = null;
for (SdkConfig sdkConfig : sdksToRun) {
last = new RobolectricFrameworkMethod(frameworkMethod.getMethod(), appManifest, sdkConfig, config);
children.add(last);
}
if (last != null) {
last.dontIncludeApiLevelInName();
}
}
return children;
}
private static void invokeAfterClass(final Class<?> clazz) throws Throwable {
final TestClass testClass = new TestClass(clazz);
final List<FrameworkMethod> afters = testClass.getAnnotatedMethods(AfterClass.class);
for (FrameworkMethod after : afters) {
after.invokeExplosively(null);
}
}
@Override
protected void runChild(FrameworkMethod method, RunNotifier notifier) {
RobolectricFrameworkMethod roboMethod = (RobolectricFrameworkMethod) method;
Description description = describeChild(method);
EachTestNotifier eachNotifier = new EachTestNotifier(notifier, description);
Config config = roboMethod.config;
if (shouldIgnore(method, config)) {
eachNotifier.fireTestIgnored();
} else {
eachNotifier.fireTestStarted();
try {
SdkConfig sdkConfig = ((RobolectricFrameworkMethod) method).sdkConfig;
InstrumentingClassLoaderFactory instrumentingClassLoaderFactory = new InstrumentingClassLoaderFactory(createClassLoaderConfig(config), getJarResolver());
SdkEnvironment sdkEnvironment = instrumentingClassLoaderFactory.getSdkEnvironment(sdkConfig);
methodBlock(method, config, roboMethod.getAppManifest(), sdkEnvironment).evaluate();
} catch (AssumptionViolatedException e) {
eachNotifier.addFailedAssumption(e);
} catch (Throwable e) {
eachNotifier.addFailure(e);
} finally {
eachNotifier.fireTestFinished();
}
}
}
/**
* Returns the ResourceProvider for the compile time SDK.
*/
@NotNull
private static PackageResourceTable getCompiletimeSdkResourceTable() {
if (compiletimeSdkResourceTable == null) {
compiletimeSdkResourceTable = ResourceTableFactory.newResourceTable("android", new ResourcePath(android.R.class, null, null));
}
return compiletimeSdkResourceTable;
}
protected boolean shouldIgnore(FrameworkMethod method, Config config) {
return method.getAnnotation(Ignore.class) != null;
}
private ParallelUniverseInterface parallelUniverseInterface;
Statement methodBlock(final FrameworkMethod method, final Config config, final AndroidManifest appManifest, final SdkEnvironment sdkEnvironment) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
// Configure shadows *BEFORE* setting the ClassLoader. This is necessary because
// creating the ShadowMap loads all ShadowProviders via ServiceLoader and this is
// not available once we install the Robolectric class loader.
configureShadows(sdkEnvironment, config);
Thread.currentThread().setContextClassLoader(sdkEnvironment.getRobolectricClassLoader());
Class bootstrappedTestClass = sdkEnvironment.bootstrappedClass(getTestClass().getJavaClass());
HelperTestRunner helperTestRunner = getHelperTestRunner(bootstrappedTestClass);
final Method bootstrappedMethod;
try {
//noinspection unchecked
bootstrappedMethod = bootstrappedTestClass.getMethod(method.getMethod().getName());
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
parallelUniverseInterface = getHooksInterface(sdkEnvironment);
try {
try {
// Only invoke @BeforeClass once per class
if (!loadedTestClasses.contains(bootstrappedTestClass)) {
invokeBeforeClass(bootstrappedTestClass);
}
assureTestLifecycle(sdkEnvironment);
parallelUniverseInterface.setSdkConfig(sdkEnvironment.getSdkConfig());
parallelUniverseInterface.resetStaticState(config);
SdkConfig sdkConfig = ((RobolectricFrameworkMethod) method).sdkConfig;
Class<?> androidBuildVersionClass = sdkEnvironment.bootstrappedClass(Build.VERSION.class);
ReflectionHelpers.setStaticField(androidBuildVersionClass, "SDK_INT", sdkConfig.getApiLevel());
ReflectionHelpers.setStaticField(androidBuildVersionClass, "RELEASE", sdkConfig.getAndroidVersion());
PackageResourceTable systemResourceTable = sdkEnvironment.getSystemResourceTable(getJarResolver());
PackageResourceTable appResourceTable = getAppResourceTable(appManifest);
parallelUniverseInterface.setUpApplicationState(bootstrappedMethod, testLifecycle, appManifest, config, new RoutingResourceTable(getCompiletimeSdkResourceTable(), appResourceTable), new RoutingResourceTable(systemResourceTable, appResourceTable), new RoutingResourceTable(systemResourceTable));
testLifecycle.beforeTest(bootstrappedMethod);
} catch (Exception e) {
throw new RuntimeException(e);
}
final Statement statement = helperTestRunner.methodBlock(new FrameworkMethod(bootstrappedMethod));
// todo: this try/finally probably isn't right -- should mimic RunAfters? [xw]
try {
statement.evaluate();
} finally {
try {
parallelUniverseInterface.tearDownApplication();
} finally {
try {
internalAfterTest(bootstrappedMethod);
} finally {
parallelUniverseInterface.resetStaticState(config); // afterward too, so stuff doesn't hold on to classes?
}
}
}
} finally {
Thread.currentThread().setContextClassLoader(RobolectricTestRunner.class.getClassLoader());
parallelUniverseInterface = null;
}
}
};
}
private void invokeBeforeClass(final Class clazz) throws Throwable {
if (!loadedTestClasses.contains(clazz)) {
loadedTestClasses.add(clazz);
final TestClass testClass = new TestClass(clazz);
final List<FrameworkMethod> befores = testClass.getAnnotatedMethods(BeforeClass.class);
for (FrameworkMethod before : befores) {
before.invokeExplosively(null);
}
}
}
protected HelperTestRunner getHelperTestRunner(Class bootstrappedTestClass) {
try {
return new HelperTestRunner(bootstrappedTestClass);
} catch (InitializationError initializationError) {
throw new RuntimeException(initializationError);
}
}
/**
* Detects which build system is in use and returns the appropriate ManifestFactory implementation.
*
* Custom TestRunner subclasses may wish to override this method to provide alternate configuration.
*
* @param config Specification of the SDK version, manifest file, package name, etc.
*/
protected ManifestFactory getManifestFactory(Config config) {
Class<?> buildConstants = config.constants();
//noinspection ConstantConditions
if (buildConstants != null && buildConstants != Void.class) {
return new GradleManifestFactory();
} else {
return new MavenManifestFactory();
}
}
protected AndroidManifest getAppManifest(Config config) {
ManifestFactory manifestFactory = getManifestFactory(config);
ManifestIdentifier identifier = manifestFactory.identify(config);
synchronized (appManifestsCache) {
AndroidManifest appManifest;
appManifest = appManifestsCache.get(identifier);
if (appManifest == null) {
appManifest = manifestFactory.create(identifier);
appManifestsCache.put(identifier, appManifest);
}
return appManifest;
}
}
/**
* Compute the effective Robolectric configuration for a given test method.
*
* Configuration information is collected from package-level <tt>robolectric.properties</tt> files
* and {@link Config} annotations on test classes, superclasses, and methods.
*
* Custom TestRunner subclasses may wish to override this method to provide alternate configuration.
*
* @param method the test method
* @return the effective Robolectric configuration for the given test method
* @since 2.0
*/
public Config getConfig(Method method) {
return configMerger.getConfig(getTestClass().getJavaClass(), method, buildGlobalConfig());
}
/**
* Provides the base Robolectric configuration {@link Config} used for all tests.
*
* Configuration provided for specific packages, test classes, and test method
* configurations will override values provided here.
*
* The returned object is likely to be reused for many tests.
*
* Custom TestRunner subclasses may wish to override this method to provide
* alternate configuration. Consider calling <code>super.buildGlobalConfig()</code>
* and overriding values as needed using a
* {@link Config.org.robolectric.annotation.Config.Builder}.
*
* The default implementation has appropriate values for most use cases.
*
* @return global {@link Config} object
* @since 3.1.3
*/
protected Config buildGlobalConfig() {
return Config.Builder.defaults().build();
}
protected void configureShadows(SdkEnvironment sdkEnvironment, Config config) {
ShadowMap shadowMap = createShadowMap();
if (config != null) {
Class<?>[] shadows = config.shadows();
if (shadows.length > 0) {
shadowMap = shadowMap.newBuilder().addShadowClasses(shadows).build();
}
}
if (InvokeDynamic.ENABLED) {
ShadowMap oldShadowMap = sdkEnvironment.replaceShadowMap(shadowMap);
Set<String> invalidatedClasses = shadowMap.getInvalidatedClasses(oldShadowMap);
sdkEnvironment.getShadowInvalidator().invalidateClasses(invalidatedClasses);
}
ClassHandler classHandler = createClassHandler(shadowMap, sdkEnvironment.getSdkConfig());
injectEnvironment(sdkEnvironment.getRobolectricClassLoader(), classHandler, sdkEnvironment.getShadowInvalidator());
}
ParallelUniverseInterface getHooksInterface(SdkEnvironment sdkEnvironment) {
ClassLoader robolectricClassLoader = sdkEnvironment.getRobolectricClassLoader();
try {
Class<?> clazz = robolectricClassLoader.loadClass(ParallelUniverse.class.getName());
Class<? extends ParallelUniverseInterface> typedClazz = clazz.asSubclass(ParallelUniverseInterface.class);
Constructor<? extends ParallelUniverseInterface> constructor = typedClazz.getConstructor(RobolectricTestRunner.class);
return constructor.newInstance(this);
} catch (ClassNotFoundException | NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
public void internalAfterTest(final Method method) {
testLifecycle.afterTest(method);
}
private void afterClass() {
testLifecycle = null;
}
@TestOnly
boolean allStateIsCleared() {
return testLifecycle == null;
}
@Override
public Object createTest() throws Exception {
throw new UnsupportedOperationException("this should always be invoked on the HelperTestRunner!");
}
private final PackageResourceTable getAppResourceTable(final AndroidManifest appManifest) {
PackageResourceTable resourceTable = appResourceTableCache.get(appManifest);
if (resourceTable == null) {
resourceTable = ResourceMerger.buildResourceTable(appManifest);
appResourceTableCache.put(appManifest, resourceTable);
}
return resourceTable;
}
protected ShadowMap createShadowMap() {
return ShadowMap.EMPTY;
}
public class HelperTestRunner extends BlockJUnit4ClassRunner {
public HelperTestRunner(Class<?> testClass) throws InitializationError {
super(testClass);
}
@Override protected Object createTest() throws Exception {
Object test = super.createTest();
testLifecycle.prepareTest(test);
return test;
}
@Override public Statement classBlock(RunNotifier notifier) {
return super.classBlock(notifier);
}
@Override public Statement methodBlock(FrameworkMethod method) {
return super.methodBlock(method);
}
@Override
protected Statement methodInvoker(FrameworkMethod method, Object test) {
final Statement invoker = super.methodInvoker(method, test);
return new Statement() {
@Override
public void evaluate() throws Throwable {
Thread orig = parallelUniverseInterface.getMainThread();
parallelUniverseInterface.setMainThread(Thread.currentThread());
try {
invoker.evaluate();
} finally {
parallelUniverseInterface.setMainThread(orig);
}
}
};
}
}
class RobolectricFrameworkMethod extends FrameworkMethod {
private final AndroidManifest appManifest;
final SdkConfig sdkConfig;
final Config config;
private boolean includeApiLevelInName = true;
RobolectricFrameworkMethod(Method method, AndroidManifest appManifest, SdkConfig sdkConfig, Config config) {
super(method);
this.appManifest = appManifest;
this.sdkConfig = sdkConfig;
this.config = config;
}
@Override
public String getName() {
// IDE focused test runs rely on preservation of the test name; we'll use the
// latest supported SDK for focused test runs
return super.getName() +
(includeApiLevelInName ? "[" + sdkConfig.getApiLevel() + "]" : "");
}
void dontIncludeApiLevelInName() {
includeApiLevelInName = false;
}
public AndroidManifest getAppManifest() {
return appManifest;
}
}
}