blob: 254be7e9c15cca1f17f39bfebcafd9fa3285a993 [file] [log] [blame]
package org.robolectric;
import android.os.Build;
import com.google.auto.service.AutoService;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
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 java.util.concurrent.atomic.AtomicInteger;
import javax.annotation.Nonnull;
import javax.annotation.Priority;
import org.junit.AssumptionViolatedException;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError;
import org.robolectric.android.AndroidInterceptors;
import org.robolectric.annotation.Config;
import org.robolectric.internal.AndroidConfigurer;
import org.robolectric.internal.AndroidSandbox;
import org.robolectric.internal.BuckManifestFactory;
import org.robolectric.internal.DefaultManifestFactory;
import org.robolectric.internal.Environment;
import org.robolectric.internal.ManifestFactory;
import org.robolectric.internal.ManifestIdentifier;
import org.robolectric.internal.MavenManifestFactory;
import org.robolectric.internal.ResourcesMode;
import org.robolectric.internal.SandboxFactory;
import org.robolectric.internal.SandboxTestRunner;
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.manifest.AndroidManifest;
import org.robolectric.pluginapi.Sdk;
import org.robolectric.pluginapi.SdkPicker;
import org.robolectric.pluginapi.config.ConfigurationStrategy;
import org.robolectric.pluginapi.config.ConfigurationStrategy.Configuration;
import org.robolectric.pluginapi.config.GlobalConfigProvider;
import org.robolectric.plugins.HierarchicalConfigurationStrategy.ConfigurationImpl;
import org.robolectric.util.PerfStatsCollector;
import org.robolectric.util.ReflectionHelpers;
import org.robolectric.util.inject.Injector;
/**
* 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 final Injector DEFAULT_INJECTOR = defaultInjector().build();
private static final Map<ManifestIdentifier, AndroidManifest> appManifestsCache = new HashMap<>();
static {
new SecureRandom(); // this starts up the Poller SunPKCS11-Darwin thread early, outside of any Robolectric classloader
}
protected static Injector.Builder defaultInjector() {
return SandboxTestRunner.defaultInjector()
.bind(Properties.class, System.getProperties());
}
private final SandboxFactory sandboxFactory;
private final SdkPicker sdkPicker;
private final ConfigurationStrategy configurationStrategy;
private ServiceLoader<ShadowProvider> providers;
private final ResModeStrategy resModeStrategy = getResModeStrategy();
private boolean alwaysIncludeVariantMarkersInName =
Boolean.parseBoolean(
System.getProperty("robolectric.alwaysIncludeVariantMarkersInTestName", "false"));
/**
* 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 {
this(testClass, DEFAULT_INJECTOR);
}
protected RobolectricTestRunner(final Class<?> testClass, Injector injector)
throws InitializationError {
super(testClass, injector);
if (DeprecatedTestRunnerDefaultConfigProvider.globalConfig == null) {
DeprecatedTestRunnerDefaultConfigProvider.globalConfig = buildGlobalConfig();
}
this.sandboxFactory = injector.getInstance(SandboxFactory.class);
this.sdkPicker = injector.getInstance(SdkPicker.class);
this.configurationStrategy = injector.getInstance(ConfigurationStrategy.class);
}
/**
* Create a {@link ClassHandler} appropriate for the given arguments.
*
* Robolectric may chose to cache the returned instance, keyed by <tt>shadowMap</tt> and <tt>sdk</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 Sdk} 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, ((AndroidSandbox) sandbox).getSdk().getApiLevel(), getInterceptors());
}
@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) {
Configuration configuration = ((RobolectricFrameworkMethod) method).getConfiguration();
Config config = configuration.get(Config.class);
Builder builder = new Builder(super.createClassLoaderConfig(method));
AndroidConfigurer.configure(builder, getInterceptors());
AndroidConfigurer.withConfig(builder, config);
return builder.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}.
*/
@Nonnull
protected Class<? extends TestLifecycle> getTestLifecycleClass() {
return DefaultTestLifecycle.class;
}
enum ResModeStrategy {
legacy,
binary,
best,
both;
static final ResModeStrategy DEFAULT = best;
private static ResModeStrategy 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 {
Configuration configuration = getConfiguration(frameworkMethod.getMethod());
AndroidManifest appManifest = getAppManifest(configuration);
List<Sdk> sdksToRun = sdkPicker.selectSdks(configuration, appManifest);
RobolectricFrameworkMethod last = null;
for (Sdk sdk : sdksToRun) {
if (resModeStrategy.includeLegacy(appManifest)) {
children.add(
last =
new RobolectricFrameworkMethod(
frameworkMethod.getMethod(),
appManifest,
sdk,
configuration,
ResourcesMode.LEGACY,
resModeStrategy,
alwaysIncludeVariantMarkersInName));
}
if (resModeStrategy.includeBinary(appManifest)) {
children.add(
last =
new RobolectricFrameworkMethod(
frameworkMethod.getMethod(),
appManifest,
sdk,
configuration,
ResourcesMode.BINARY,
resModeStrategy,
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
@Nonnull
protected AndroidSandbox getSandbox(FrameworkMethod method) {
RobolectricFrameworkMethod roboMethod = (RobolectricFrameworkMethod) method;
Sdk sdk = roboMethod.getSdk();
InstrumentationConfiguration classLoaderConfig = createClassLoaderConfig(method);
ResourcesMode resourcesMode = roboMethod.getResourcesMode();
if (resourcesMode == ResourcesMode.LEGACY && sdk.getApiLevel() > Build.VERSION_CODES.P) {
throw new AssumptionViolatedException("Robolectric doesn't support legacy mode after P");
}
if (sdk.isKnown() && !sdk.isSupported()) {
throw new AssumptionViolatedException(
"Failed to create a Robolectric sandbox: " + sdk.getUnsupportedMessage());
} else {
return sandboxFactory.getAndroidSandbox(classLoaderConfig, sdk, resourcesMode);
}
}
@Override
protected void beforeTest(Sandbox sandbox, FrameworkMethod method, Method bootstrappedMethod) throws Throwable {
AndroidSandbox androidSandbox = (AndroidSandbox) sandbox;
RobolectricFrameworkMethod roboMethod = (RobolectricFrameworkMethod) method;
PerfStatsCollector perfStatsCollector = PerfStatsCollector.getInstance();
Sdk sdk = roboMethod.getSdk();
perfStatsCollector.putMetadata(
AndroidMetadata.class,
new AndroidMetadata(
ImmutableMap.of("ro.build.version.sdk", "" + sdk.getApiLevel()),
roboMethod.resourcesMode.name()));
System.out.println(
"[Robolectric] " + roboMethod.getDeclaringClass().getName() + "."
+ roboMethod.getMethod().getName() + ": sdk=" + sdk.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.setStuff(androidSandbox, androidSandbox.getEnvironment());
Class<TestLifecycle> cl = androidSandbox.bootstrappedClass(getTestLifecycleClass());
roboMethod.testLifecycle = ReflectionHelpers.newInstance(cl);
providers = ServiceLoader.load(ShadowProvider.class, androidSandbox.getRobolectricClassLoader());
AndroidManifest appManifest = roboMethod.getAppManifest();
roboMethod.getEnvironment().setUpApplicationState(
bootstrappedMethod,
roboMethod.getConfiguration(), appManifest
);
roboMethod.testLifecycle.beforeTest(bootstrappedMethod);
}
@Override
protected void afterTest(FrameworkMethod method, Method bootstrappedMethod) {
RobolectricFrameworkMethod roboMethod = (RobolectricFrameworkMethod) method;
try {
roboMethod.getEnvironment().tearDownApplication();
} finally {
roboMethod.testLifecycle.afterTest(bootstrappedMethod);
}
}
@Override
protected void finallyAfterTest(FrameworkMethod method) {
RobolectricFrameworkMethod roboMethod = (RobolectricFrameworkMethod) 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)", () -> {
// TODO: roboMethod.sandbox.resetState(); instead
if (providers != null) {
for (ShadowProvider provider : providers) {
provider.reset();
}
}
});
} finally {
roboMethod.testLifecycle = null;
roboMethod.clearContext();
}
}
@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() {
return staticGetBuildSystemApiProperties();
}
protected static Properties staticGetBuildSystemApiProperties() {
try (InputStream resourceAsStream =
RobolectricTestRunner.class.getResourceAsStream(
"/com/android/tools/test_config.properties")) {
if (resourceAsStream == null) {
return null;
}
Properties properties = new Properties();
properties.load(resourceAsStream);
return properties;
} catch (IOException e) {
return null;
}
}
private AndroidManifest getAppManifest(Configuration configuration) {
Config config = configuration.get(Config.class);
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
* @deprecated Provide an implementation of {@link javax.inject.Provider<Config>} instead. See
* [Migration Notes](http://robolectric.org/migrating/#migrating-to-40) for details. This
* method will be removed in Robolectric 4.3.
* @since 2.0
*/
@Deprecated
public Config getConfig(Method method) {
throw new UnsupportedOperationException();
}
/**
* Calculate the configuration for a given test method.
*
* Temporarily visible for migration.
* @deprecated Going away before 4.2. DO NOT SHIP.
*/
@Deprecated
protected Configuration getConfiguration(Method method) {
Configuration configuration =
configurationStrategy.getConfig(getTestClass().getJavaClass(), method);
// in case #getConfig(Method) has been overridden...
try {
Config config = getConfig(method);
((ConfigurationImpl) configuration).put(Config.class, config);
} catch (UnsupportedOperationException e) {
// no problem
}
return configuration;
}
/**
* 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
* @deprecated Provide a service implementation of {@link GlobalConfigProvider} instead. See
* [Migration Notes](http://robolectric.org/migrating/#migrating-to-40) for details. This
* method will be removed in Robolectric 4.3.
* @since 3.1.3
*/
@Deprecated
protected Config buildGlobalConfig() {
return new Config.Builder().build();
}
@AutoService(GlobalConfigProvider.class)
@Priority(Integer.MIN_VALUE)
@Deprecated
public static class DeprecatedTestRunnerDefaultConfigProvider implements GlobalConfigProvider {
static Config globalConfig;
@Override
public Config get() {
return globalConfig;
}
}
@Override @Nonnull
protected Class<?>[] getExtraShadows(FrameworkMethod frameworkMethod) {
Config config = ((RobolectricFrameworkMethod) frameworkMethod).getConfiguration().get(Config.class);
return config.shadows();
}
@Override
protected void afterClass() {
}
@Override
public Object createTest() throws Exception {
throw new UnsupportedOperationException("this should always be invoked on the HelperTestRunner!");
}
@VisibleForTesting
ResModeStrategy getResModeStrategy() {
return ResModeStrategy.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;
}
}
/**
* Fields in this class must be serializable using [XStream](https://x-stream.github.io/).
*/
static final class RobolectricFrameworkMethod extends FrameworkMethod {
private static final AtomicInteger NEXT_ID = new AtomicInteger();
private static final Map<Integer, TestExecutionContext> CONTEXT = new HashMap<>();
private final int id;
@Nonnull private final AndroidManifest appManifest;
@Nonnull private final Configuration configuration;
@Nonnull private final ResourcesMode resourcesMode;
@Nonnull private final ResModeStrategy defaultResModeStrategy;
private final boolean alwaysIncludeVariantMarkersInName;
private boolean includeVariantMarkersInTestName = true;
TestLifecycle testLifecycle;
RobolectricFrameworkMethod(
@Nonnull Method method,
@Nonnull AndroidManifest appManifest,
@Nonnull Sdk sdk,
@Nonnull Configuration configuration,
@Nonnull ResourcesMode resourcesMode,
@Nonnull ResModeStrategy defaultResModeStrategy,
boolean alwaysIncludeVariantMarkersInName) {
super(method);
this.appManifest = appManifest;
this.configuration = configuration;
this.resourcesMode = resourcesMode;
this.defaultResModeStrategy = defaultResModeStrategy;
this.alwaysIncludeVariantMarkersInName = alwaysIncludeVariantMarkersInName;
// external storage for things that can't go through a serialization cycle e.g. for PowerMock.
this.id = NEXT_ID.getAndIncrement();
CONTEXT.put(id, new TestExecutionContext(sdk));
}
@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(getSdk().getApiLevel()).append("]");
if (defaultResModeStrategy == ResModeStrategy.both) {
buf.append("[").append(resourcesMode.name()).append("]");
}
}
return buf.toString();
}
void dontIncludeVariantMarkersInTestName() {
includeVariantMarkersInTestName = false;
}
@Nonnull
AndroidManifest getAppManifest() {
return appManifest;
}
@Nonnull
public Sdk getSdk() {
return getContext().sdk;
}
void setStuff(Sandbox sandbox, Environment environment) {
TestExecutionContext context = getContext();
context.sandbox = sandbox;
context.environment = environment;
}
Sandbox getSandbox() {
return getContext().sandbox;
}
Environment getEnvironment() {
TestExecutionContext context = getContext();
return context == null ? null : context.environment;
}
public boolean isLegacy() {
return resourcesMode == ResourcesMode.LEGACY;
}
public ResourcesMode getResourcesMode() {
return resourcesMode;
}
private TestExecutionContext getContext() {
return CONTEXT.get(id);
}
private void clearContext() {
CONTEXT.remove(id);
}
@Override
protected void finalize() throws Throwable {
super.finalize();
clearContext();
}
@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 getSdk().equals(that.getSdk()) && resourcesMode == that.resourcesMode;
}
@Override
public int hashCode() {
int result = super.hashCode();
result = 31 * result + getSdk().hashCode();
result = 31 * result + resourcesMode.ordinal();
return result;
}
@Override
public String toString() {
return getName();
}
@Nonnull
public Configuration getConfiguration() {
return configuration;
}
private static class TestExecutionContext {
private final Sdk sdk;
private Sandbox sandbox;
private Environment environment;
TestExecutionContext(Sdk sdk) {
this.sdk = sdk;
}
}
}
}