blob: b88c16cb0fafab5d2ffe8745d02974d53f0d6109 [file] [log] [blame]
package org.robolectric;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterators;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.ServiceLoader;
import javax.annotation.Nonnull;
import org.junit.Ignore;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.Statement;
import org.robolectric.android.AndroidInterceptors;
import org.robolectric.android.internal.ParallelUniverse;
import org.robolectric.annotation.Config;
import org.robolectric.internal.AndroidConfigurer;
import org.robolectric.internal.BuckManifestFactory;
import org.robolectric.internal.DefaultManifestFactory;
import org.robolectric.internal.ManifestFactory;
import org.robolectric.internal.ManifestIdentifier;
import org.robolectric.internal.MavenManifestFactory;
import org.robolectric.internal.ParallelUniverseInterface;
import org.robolectric.internal.SandboxFactory;
import org.robolectric.internal.SandboxTestRunner;
import org.robolectric.internal.SdkConfig;
import org.robolectric.internal.SdkEnvironment;
import org.robolectric.internal.ShadowProvider;
import org.robolectric.internal.bytecode.ClassHandler;
import org.robolectric.internal.bytecode.InstrumentationConfiguration;
import org.robolectric.internal.bytecode.InstrumentationConfiguration.Builder;
import org.robolectric.internal.bytecode.Interceptor;
import org.robolectric.internal.bytecode.Sandbox;
import org.robolectric.internal.bytecode.SandboxClassLoader;
import org.robolectric.internal.bytecode.ShadowMap;
import org.robolectric.internal.bytecode.ShadowWrangler;
import org.robolectric.internal.dependency.CachedDependencyResolver;
import org.robolectric.internal.dependency.DependencyResolver;
import org.robolectric.internal.dependency.LocalDependencyResolver;
import org.robolectric.internal.dependency.PropertiesDependencyResolver;
import org.robolectric.manifest.AndroidManifest;
import org.robolectric.res.Fs;
import org.robolectric.util.Logger;
import org.robolectric.util.PerfStatsCollector;
import org.robolectric.util.ReflectionHelpers;
/**
* Loads and runs a test in a {@link SandboxClassLoader} in order to
* provide a simulation of the Android runtime environment.
*/
@SuppressWarnings("NewApi")
public class RobolectricTestRunner extends SandboxTestRunner {
public static final String CONFIG_PROPERTIES = "robolectric.properties";
private static ApkLoader apkLoader;
private static final Map<ManifestIdentifier, AndroidManifest> appManifestsCache = new HashMap<>();
private final SdkPicker sdkPicker;
private final ConfigMerger configMerger;
private ServiceLoader<ShadowProvider> providers;
private transient DependencyResolver dependencyResolver;
private final ResourcesMode resourcesMode = getResourcesMode();
private boolean alwaysIncludeVariantMarkersInName =
Boolean.parseBoolean(
System.getProperty("robolectric.alwaysIncludeVariantMarkersInTestName", "false"));
static {
new SecureRandom(); // this starts up the Poller SunPKCS11-Darwin thread early, outside of any Robolectric classloader
}
/**
* Creates a runner to run {@code testClass}. 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();
synchronized (RobolectricTestRunner.class) {
if (apkLoader == null) {
apkLoader = new ApkLoader(getJarResolver());
}
}
}
protected DependencyResolver getJarResolver() {
if (dependencyResolver == null) {
if (Boolean.getBoolean("robolectric.offline")) {
String propPath = System.getProperty("robolectric-deps.properties");
if (propPath != null) {
try {
dependencyResolver = new PropertiesDependencyResolver(Paths.get(propPath), null);
} catch (IOException e) {
throw new RuntimeException("couldn't read dependencies" , e);
}
} else {
String dependencyDir = System.getProperty("robolectric.dependency.dir", ".");
dependencyResolver = new LocalDependencyResolver(new File(dependencyDir));
}
} else {
// cacheDir bumped to 'robolectric-2' to invalidate caching of bad URLs on windows prior
// to fix for https://github.com/robolectric/robolectric/issues/3955
File cacheDir = new File(new File(System.getProperty("java.io.tmpdir")), "robolectric-2");
Class<?> mavenDependencyResolverClass = ReflectionHelpers.loadClass(RobolectricTestRunner.class.getClassLoader(),
"org.robolectric.internal.dependency.MavenDependencyResolver");
DependencyResolver dependencyResolver = (DependencyResolver) ReflectionHelpers.callConstructor(mavenDependencyResolverClass);
if (cacheDir.exists() || cacheDir.mkdir()) {
Logger.info("Dependency cache location: %s", cacheDir.getAbsolutePath());
this.dependencyResolver = new CachedDependencyResolver(dependencyResolver, cacheDir, 60 * 60 * 24 * 1000);
} else {
this.dependencyResolver = dependencyResolver;
}
}
URL buildPathPropertiesUrl = getClass().getClassLoader().getResource("robolectric-deps.properties");
if (buildPathPropertiesUrl != null) {
Logger.info("Using Robolectric classes from %s", buildPathPropertiesUrl.getPath());
Path propertiesFile = Paths.get(Fs.toUri(buildPathPropertiesUrl));
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 sandbox the {@link SdkConfig} in effect for this test
* @return an appropriate {@link ClassHandler}. This implementation returns a {@link ShadowWrangler}.
* @since 2.3
*/
@Override
@Nonnull
protected ClassHandler createClassHandler(ShadowMap shadowMap, Sandbox sandbox) {
return new ShadowWrangler(shadowMap, ((SdkEnvironment) sandbox).getSdkConfig().getApiLevel(), getInterceptors());
}
/**
* Create a {@link ConfigMerger} for calculating the {@link Config} tests.
*
* Alternate implementations may be provided using a ServiceLoader.
*
* @return a {@link ConfigMerger}
* @since 3.2
*/
@Nonnull
private ConfigMerger createConfigMerger() {
ServiceLoader<ConfigMerger> serviceLoader = ServiceLoader.load(ConfigMerger.class);
ConfigMerger merger;
if (serviceLoader != null && serviceLoader.iterator().hasNext()) {
merger = Iterators.getOnlyElement(serviceLoader.iterator());
} else {
merger = new ConfigMerger();
}
return merger;
}
/**
* 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
*/
@Nonnull
protected SdkPicker createSdkPicker() {
return new SdkPicker(
SdkConfig.getSupportedSdkConfigs(),
SdkPicker.enumerateEnabledSdks(System.getProperty("robolectric.enabledSdks")));
}
@Override
@Nonnull // todo
protected Collection<Interceptor> findInterceptors() {
return AndroidInterceptors.all();
}
/**
* Create an {@link InstrumentationConfiguration} suitable for the provided
* {@link FrameworkMethod}.
*
* Adds configuration for Android using {@link AndroidConfigurer}.
*
* Custom TestRunner subclasses may wish to override this method to provide additional
* configuration.
*
* @param method the test method that's about to run
* @return an {@link InstrumentationConfiguration}
*/
@Override @Nonnull
protected InstrumentationConfiguration createClassLoaderConfig(final FrameworkMethod method) {
Builder builder = new Builder(super.createClassLoaderConfig(method));
AndroidConfigurer.configure(builder, getInterceptors());
AndroidConfigurer.withConfig(builder, ((RobolectricFrameworkMethod) method).config);
return builder.build();
}
@Override
protected void configureSandbox(Sandbox sandbox, FrameworkMethod method) {
SdkEnvironment sdkEnvironment = (SdkEnvironment) sandbox;
RobolectricFrameworkMethod roboMethod = (RobolectricFrameworkMethod) method;
boolean isLegacy = roboMethod.isLegacy();
roboMethod.parallelUniverseInterface = getHooksInterface(sdkEnvironment);
roboMethod.parallelUniverseInterface.setSdkConfig(roboMethod.sdkConfig);
roboMethod.parallelUniverseInterface.setResourcesMode(isLegacy);
super.configureSandbox(sandbox, method);
}
/**
* 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}.
*/
@Nonnull
protected Class<? extends TestLifecycle> getTestLifecycleClass() {
return DefaultTestLifecycle.class;
}
enum ResourcesMode {
legacy,
binary,
best,
both;
static final ResourcesMode DEFAULT = best;
private static ResourcesMode getFromProperties() {
String resourcesMode = System.getProperty("robolectric.resourcesMode");
return resourcesMode == null ? DEFAULT : valueOf(resourcesMode);
}
boolean includeLegacy(AndroidManifest appManifest) {
return appManifest.supportsLegacyResourcesMode()
&&
(this == legacy
|| (this == best && !appManifest.supportsBinaryResourcesMode())
|| this == both);
}
boolean includeBinary(AndroidManifest appManifest) {
return appManifest.supportsBinaryResourcesMode()
&& (this == binary || this == best || this == both);
}
}
@Override
protected List<FrameworkMethod> getChildren() {
List<FrameworkMethod> children = new ArrayList<>();
for (FrameworkMethod frameworkMethod : super.getChildren()) {
try {
Config config = getConfig(frameworkMethod.getMethod());
AndroidManifest appManifest = getAppManifest(config);
List<SdkConfig> sdksToRun = sdkPicker.selectSdks(config, appManifest);
RobolectricFrameworkMethod last = null;
for (SdkConfig sdkConfig : sdksToRun) {
if (resourcesMode.includeLegacy(appManifest)) {
children.add(
last =
new RobolectricFrameworkMethod(
frameworkMethod.getMethod(),
appManifest,
sdkConfig,
config,
ResourcesMode.legacy,
resourcesMode,
alwaysIncludeVariantMarkersInName));
}
if (resourcesMode.includeBinary(appManifest)) {
children.add(
last =
new RobolectricFrameworkMethod(
frameworkMethod.getMethod(),
appManifest,
sdkConfig,
config,
ResourcesMode.binary,
resourcesMode,
alwaysIncludeVariantMarkersInName));
}
}
if (last != null) {
last.dontIncludeVariantMarkersInTestName();
}
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("failed to configure " +
getTestClass().getName() + "." + frameworkMethod.getMethod().getName() +
": " + e.getMessage(), e);
}
}
return children;
}
@Override protected boolean shouldIgnore(FrameworkMethod method) {
return method.getAnnotation(Ignore.class) != null;
}
@Override
@Nonnull
protected SdkEnvironment getSandbox(FrameworkMethod method) {
RobolectricFrameworkMethod roboMethod = (RobolectricFrameworkMethod) method;
SdkConfig sdkConfig = roboMethod.sdkConfig;
return getSandboxFactory().getSdkEnvironment(
createClassLoaderConfig(method), sdkConfig, roboMethod.isLegacy(), getJarResolver());
}
protected SandboxFactory getSandboxFactory() {
return SandboxFactory.INSTANCE;
}
@Override
protected void beforeTest(Sandbox sandbox, FrameworkMethod method, Method bootstrappedMethod) throws Throwable {
SdkEnvironment sdkEnvironment = (SdkEnvironment) sandbox;
RobolectricFrameworkMethod roboMethod = (RobolectricFrameworkMethod) method;
PerfStatsCollector perfStatsCollector = PerfStatsCollector.getInstance();
SdkConfig sdkConfig = roboMethod.sdkConfig;
perfStatsCollector.putMetadata(
AndroidMetadata.class,
new AndroidMetadata(
ImmutableMap.of("ro.build.version.sdk", "" + sdkConfig.getApiLevel()),
roboMethod.resourcesMode.name()));
System.out.println(
"[Robolectric] " + roboMethod.getDeclaringClass().getName() + "."
+ roboMethod.getMethod().getName() + ": sdk=" + sdkConfig.getApiLevel()
+ "; resources=" + roboMethod.resourcesMode);
if (roboMethod.resourcesMode == ResourcesMode.legacy) {
System.out.println(
"[Robolectric] NOTICE: legacy resources mode is deprecated; see http://robolectric.org/migrating/#migrating-to-40");
}
roboMethod.parallelUniverseInterface = getHooksInterface(sdkEnvironment);
Class<TestLifecycle> cl = sdkEnvironment.bootstrappedClass(getTestLifecycleClass());
roboMethod.testLifecycle = ReflectionHelpers.newInstance(cl);
providers = ServiceLoader.load(ShadowProvider.class, sdkEnvironment.getRobolectricClassLoader());
roboMethod.parallelUniverseInterface.setSdkConfig(sdkConfig);
AndroidManifest appManifest = roboMethod.getAppManifest();
roboMethod.parallelUniverseInterface.setUpApplicationState(
apkLoader,
bootstrappedMethod,
roboMethod.config, appManifest,
sdkEnvironment
);
roboMethod.testLifecycle.beforeTest(bootstrappedMethod);
}
@Override
protected void afterTest(FrameworkMethod method, Method bootstrappedMethod) {
RobolectricFrameworkMethod roboMethod = (RobolectricFrameworkMethod) method;
try {
roboMethod.parallelUniverseInterface.tearDownApplication();
} finally {
internalAfterTest(method, bootstrappedMethod);
}
}
private void resetStaticState() {
for (ShadowProvider provider : providers) {
provider.reset();
}
}
@Override
protected void finallyAfterTest(FrameworkMethod method) {
// If the test was interrupted, it will interfere with new AbstractInterruptibleChannels in
// subsequent tests, e.g. created by Files.newInputStream(), so clear it and warn.
if (Thread.interrupted()) {
System.out.println("WARNING: Test thread was interrupted! " + method.toString());
}
try {
// reset static state afterward too, so statics don't defeat GC?
PerfStatsCollector.getInstance()
.measure("reset Android state (after test)", this::resetStaticState);
} finally {
RobolectricFrameworkMethod roboMethod = (RobolectricFrameworkMethod) method;
roboMethod.testLifecycle = null;
roboMethod.parallelUniverseInterface = null;
}
}
@Override protected SandboxTestRunner.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) {
Properties buildSystemApiProperties = getBuildSystemApiProperties();
if (buildSystemApiProperties != null) {
return new DefaultManifestFactory(buildSystemApiProperties);
}
if (BuckManifestFactory.isBuck()) {
return new BuckManifestFactory();
} else {
return new MavenManifestFactory();
}
}
protected Properties getBuildSystemApiProperties() {
InputStream resourceAsStream = getClass().getResourceAsStream("/com/android/tools/test_config.properties");
if (resourceAsStream == null) {
return null;
}
try {
Properties properties = new Properties();
properties.load(resourceAsStream);
return properties;
} catch (IOException e) {
return null;
} finally {
try {
resourceAsStream.close();
} catch (IOException e) {
// ignore
}
}
}
private AndroidManifest getAppManifest(Config config) {
ManifestFactory manifestFactory = getManifestFactory(config);
ManifestIdentifier identifier = manifestFactory.identify(config);
return cachedCreateAppManifest(identifier);
}
private AndroidManifest cachedCreateAppManifest(ManifestIdentifier identifier) {
synchronized (appManifestsCache) {
AndroidManifest appManifest;
appManifest = appManifestsCache.get(identifier);
if (appManifest == null) {
appManifest = createAndroidManifest(identifier);
appManifestsCache.put(identifier, appManifest);
}
return appManifest;
}
}
/**
* Internal use only.
* @deprecated Do not use.
*/
@Deprecated
@VisibleForTesting
public static AndroidManifest createAndroidManifest(ManifestIdentifier manifestIdentifier) {
List<ManifestIdentifier> libraries = manifestIdentifier.getLibraries();
List<AndroidManifest> libraryManifests = new ArrayList<>();
for (ManifestIdentifier library : libraries) {
libraryManifests.add(createAndroidManifest(library));
}
return new AndroidManifest(manifestIdentifier.getManifestFile(), manifestIdentifier.getResDir(),
manifestIdentifier.getAssetDir(), libraryManifests, manifestIdentifier.getPackageName(),
manifestIdentifier.getApkFile());
}
/**
* 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.
*
* Custom TestRunner subclasses may wish to override this method to provide
* alternate configuration. Consider using a {@link 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 new Config.Builder().build();
}
@Override @Nonnull
protected Class<?>[] getExtraShadows(FrameworkMethod frameworkMethod) {
Config config = ((RobolectricFrameworkMethod) frameworkMethod).config;
return config.shadows();
}
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();
return constructor.newInstance();
} catch (ClassNotFoundException | NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
protected void internalAfterTest(FrameworkMethod frameworkMethod, Method method) {
RobolectricFrameworkMethod roboMethod = (RobolectricFrameworkMethod) frameworkMethod;
roboMethod.testLifecycle.afterTest(method);
}
@Override
protected void afterClass() {
}
@Override
public Object createTest() throws Exception {
throw new UnsupportedOperationException("this should always be invoked on the HelperTestRunner!");
}
@VisibleForTesting
ResourcesMode getResourcesMode() {
return ResourcesMode.getFromProperties();
}
public static class HelperTestRunner extends SandboxTestRunner.HelperTestRunner {
public HelperTestRunner(Class bootstrappedTestClass) throws InitializationError {
super(bootstrappedTestClass);
}
@Override protected Object createTest() throws Exception {
Object test = super.createTest();
RobolectricFrameworkMethod roboMethod = (RobolectricFrameworkMethod) this.frameworkMethod;
roboMethod.testLifecycle.prepareTest(test);
return test;
}
@Override
protected Statement methodInvoker(FrameworkMethod method, Object test) {
final Statement invoker = super.methodInvoker(method, test);
final RobolectricFrameworkMethod roboMethod = (RobolectricFrameworkMethod) this.frameworkMethod;
return new Statement() {
@Override
public void evaluate() throws Throwable {
Thread orig = roboMethod.parallelUniverseInterface.getMainThread();
roboMethod.parallelUniverseInterface.setMainThread(Thread.currentThread());
try {
invoker.evaluate();
} finally {
roboMethod.parallelUniverseInterface.setMainThread(orig);
}
}
};
}
}
static class RobolectricFrameworkMethod extends FrameworkMethod {
private final @Nonnull AndroidManifest appManifest;
final @Nonnull SdkConfig sdkConfig;
final @Nonnull Config config;
final ResourcesMode resourcesMode;
private final ResourcesMode defaultResourcesMode;
private final boolean alwaysIncludeVariantMarkersInName;
private boolean includeVariantMarkersInTestName = true;
TestLifecycle testLifecycle;
ParallelUniverseInterface parallelUniverseInterface;
RobolectricFrameworkMethod(
@Nonnull Method method,
@Nonnull AndroidManifest appManifest,
@Nonnull SdkConfig sdkConfig,
@Nonnull Config config,
ResourcesMode resourcesMode,
ResourcesMode defaultResourcesMode,
boolean alwaysIncludeVariantMarkersInName) {
super(method);
this.appManifest = appManifest;
this.sdkConfig = sdkConfig;
this.config = config;
this.resourcesMode = resourcesMode;
this.defaultResourcesMode = defaultResourcesMode;
this.alwaysIncludeVariantMarkersInName = alwaysIncludeVariantMarkersInName;
}
@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
StringBuilder buf = new StringBuilder(super.getName());
if (includeVariantMarkersInTestName || alwaysIncludeVariantMarkersInName) {
buf.append("[").append(sdkConfig.getApiLevel()).append("]");
if (defaultResourcesMode == ResourcesMode.both) {
buf.append("[").append(resourcesMode.name()).append("]");
}
}
return buf.toString();
}
void dontIncludeVariantMarkersInTestName() {
includeVariantMarkersInTestName = false;
}
@Nonnull
AndroidManifest getAppManifest() {
return appManifest;
}
public boolean isLegacy() {
return resourcesMode == ResourcesMode.legacy;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
RobolectricFrameworkMethod that = (RobolectricFrameworkMethod) o;
return sdkConfig.equals(that.sdkConfig) && resourcesMode == that.resourcesMode;
}
@Override
public int hashCode() {
int result = super.hashCode();
result = 31 * result + sdkConfig.hashCode();
result = 31 * result + resourcesMode.ordinal();
return result;
}
@Override
public String toString() {
return getName();
}
}
}