blob: bc38f074080cd0923417360a583dedfaa8c69cdc [file] [log] [blame]
package org.robolectric.internal;
import static java.util.Arrays.asList;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.ServiceLoader;
import javax.annotation.Nonnull;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.internal.runners.statements.FailOnTimeout;
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.internal.bytecode.ClassHandler;
import org.robolectric.internal.bytecode.InstrumentationConfiguration;
import org.robolectric.internal.bytecode.Interceptor;
import org.robolectric.internal.bytecode.Interceptors;
import org.robolectric.internal.bytecode.Sandbox;
import org.robolectric.internal.bytecode.SandboxClassLoader;
import org.robolectric.internal.bytecode.SandboxConfig;
import org.robolectric.internal.bytecode.ShadowInfo;
import org.robolectric.internal.bytecode.ShadowMap;
import org.robolectric.internal.bytecode.ShadowWrangler;
import org.robolectric.pluginapi.perf.Metadata;
import org.robolectric.pluginapi.perf.Metric;
import org.robolectric.pluginapi.perf.PerfStatsReporter;
import org.robolectric.util.PerfStatsCollector;
import org.robolectric.util.PerfStatsCollector.Event;
import org.robolectric.util.inject.Injector;
import org.robolectric.util.reflector.UnsafeAccess;
@SuppressWarnings("NewApi")
public class SandboxTestRunner extends BlockJUnit4ClassRunner {
private static final ShadowMap BASE_SHADOW_MAP;
private static final Injector DEFAULT_INJECTOR = defaultInjector().build();
static {
ServiceLoader<ShadowProvider> shadowProviders = ServiceLoader.load(ShadowProvider.class);
BASE_SHADOW_MAP = ShadowMap.createFromShadowProviders(shadowProviders);
}
protected static Injector.Builder defaultInjector() {
return new Injector.Builder();
}
private final Interceptors interceptors;
private final List<PerfStatsReporter> perfStatsReporters;
private final HashSet<Class<?>> loadedTestClasses = new HashSet<>();
public SandboxTestRunner(Class<?> klass) throws InitializationError {
this(klass, DEFAULT_INJECTOR);
}
public SandboxTestRunner(Class<?> klass, Injector injector) throws InitializationError {
super(klass);
interceptors = new Interceptors(findInterceptors());
perfStatsReporters = Arrays.asList(injector.getInstance(PerfStatsReporter[].class));
}
@Nonnull
protected Collection<Interceptor> findInterceptors() {
return Collections.emptyList();
}
@Nonnull
protected Interceptors getInterceptors() {
return interceptors;
}
@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();
}
}
};
}
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);
}
}
}
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);
}
}
protected void afterClass() {
}
@Nonnull
protected Sandbox getSandbox(FrameworkMethod method) {
InstrumentationConfiguration instrumentationConfiguration = createClassLoaderConfig(method);
ClassLoader sandboxClassLoader = new SandboxClassLoader(ClassLoader.getSystemClassLoader(), instrumentationConfiguration);
return new Sandbox(sandboxClassLoader);
}
/**
* Create an {@link InstrumentationConfiguration} suitable for the provided {@link FrameworkMethod}.
*
* Custom TestRunner subclasses may wish to override this method to provide alternate configuration.
*
* @param method the test method that's about to run
* @return an {@link InstrumentationConfiguration}
*/
@Nonnull
protected InstrumentationConfiguration createClassLoaderConfig(FrameworkMethod method) {
InstrumentationConfiguration.Builder builder = InstrumentationConfiguration.newBuilder()
.doNotAcquirePackage("java.")
.doNotAcquirePackage("sun.")
.doNotAcquirePackage("org.robolectric.annotation.")
.doNotAcquirePackage("org.robolectric.internal.")
.doNotAcquirePackage("org.robolectric.pluginapi.")
.doNotAcquirePackage("org.robolectric.util.")
.doNotAcquirePackage("org.junit.");
String customPackages = System.getProperty("org.robolectric.packagesToNotAcquire", "");
for (String pkg : customPackages.split(",")) {
if (!pkg.isEmpty()) {
builder.doNotAcquirePackage(pkg);
}
}
for (Class<?> shadowClass : getExtraShadows(method)) {
ShadowInfo shadowInfo = ShadowMap.obtainShadowInfo(shadowClass);
builder.addInstrumentedClass(shadowInfo.shadowedClassName);
}
addInstrumentedPackages(method, builder);
return builder.build();
}
private void addInstrumentedPackages(FrameworkMethod method, InstrumentationConfiguration.Builder builder) {
SandboxConfig classConfig = getTestClass().getJavaClass().getAnnotation(SandboxConfig.class);
if (classConfig != null) {
for (String pkgName : classConfig.instrumentedPackages()) {
builder.addInstrumentedPackage(pkgName);
}
}
SandboxConfig methodConfig = method.getAnnotation(SandboxConfig.class);
if (methodConfig != null) {
for (String pkgName : methodConfig.instrumentedPackages()) {
builder.addInstrumentedPackage(pkgName);
}
}
}
protected void configureSandbox(Sandbox sandbox, FrameworkMethod method) {
ShadowMap.Builder builder = createShadowMap().newBuilder();
// 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.
Class<?>[] shadows = getExtraShadows(method);
if (shadows.length > 0) {
builder.addShadowClasses(shadows);
}
ShadowMap shadowMap = builder.build();
sandbox.replaceShadowMap(shadowMap);
sandbox.configure(createClassHandler(shadowMap, sandbox), getInterceptors());
}
@Override protected Statement methodBlock(final FrameworkMethod method) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
PerfStatsCollector perfStatsCollector = PerfStatsCollector.getInstance();
perfStatsCollector.reset();
perfStatsCollector.setEnabled(!perfStatsReporters.isEmpty());
Event initialization = perfStatsCollector.startEvent("initialization");
Sandbox sandbox = getSandbox(method);
// Configure sandbox *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.
configureSandbox(sandbox, method);
sandbox.runOnMainThread(() -> {
ClassLoader priorContextClassLoader = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(sandbox.getRobolectricClassLoader());
Class bootstrappedTestClass =
sandbox.bootstrappedClass(getTestClass().getJavaClass());
HelperTestRunner helperTestRunner = getHelperTestRunner(bootstrappedTestClass);
helperTestRunner.frameworkMethod = method;
final Method bootstrappedMethod;
try {
//noinspection unchecked
bootstrappedMethod = bootstrappedTestClass.getMethod(method.getMethod().getName());
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
try {
// Only invoke @BeforeClass once per class
invokeBeforeClass(bootstrappedTestClass);
beforeTest(sandbox, method, bootstrappedMethod);
initialization.finished();
Statement statement =
helperTestRunner.methodBlock(new FrameworkMethod(bootstrappedMethod));
// todo: this try/finally probably isn't right -- should mimic RunAfters? [xw]
try {
statement.evaluate();
} finally {
afterTest(method, bootstrappedMethod);
}
} catch (Throwable throwable) {
UnsafeAccess.throwException(throwable);
} finally {
Thread.currentThread().setContextClassLoader(priorContextClassLoader);
try {
finallyAfterTest(method);
} catch (Exception e) {
e.printStackTrace();
}
}
});
reportPerfStats(perfStatsCollector);
perfStatsCollector.reset();
}
};
}
private void reportPerfStats(PerfStatsCollector perfStatsCollector) {
if (perfStatsReporters.isEmpty()) {
return;
}
Metadata metadata = perfStatsCollector.getMetadata();
Collection<Metric> metrics = perfStatsCollector.getMetrics();
for (PerfStatsReporter perfStatsReporter : perfStatsReporters) {
try {
perfStatsReporter.report(metadata, metrics);
} catch (Exception e) {
e.printStackTrace();
}
}
}
protected void beforeTest(Sandbox sandbox, FrameworkMethod method, Method bootstrappedMethod) throws Throwable {
}
protected void afterTest(FrameworkMethod method, Method bootstrappedMethod) {
}
protected void finallyAfterTest(FrameworkMethod method) {
}
protected HelperTestRunner getHelperTestRunner(Class bootstrappedTestClass) {
try {
return new HelperTestRunner(bootstrappedTestClass);
} catch (InitializationError initializationError) {
throw new RuntimeException(initializationError);
}
}
protected static class HelperTestRunner extends BlockJUnit4ClassRunner {
public FrameworkMethod frameworkMethod;
public HelperTestRunner(Class<?> klass) throws InitializationError {
super(klass);
}
// for visibility from SandboxTestRunner.methodBlock()
@Override
protected Statement methodBlock(FrameworkMethod method) {
return super.methodBlock(method);
}
/**
* For tests with a timeout, we need to wrap the test method execution (but not befores or
* afters) in a {@link TimeLimitedStatement}. We can't use JUnit's built-in
* {@link FailOnTimeout} statement because it causes the test to be run on a new thread, but
* tests should be run on the sandbox's main thread in all cases.
*/
@Override
protected Statement methodInvoker(FrameworkMethod method, Object test) {
Statement delegate = super.methodInvoker(method, test);
long timeout = getTimeout(method.getAnnotation(Test.class));
if (timeout == 0) {
return delegate;
} else {
return new TimeLimitedStatement(timeout, delegate);
}
}
/**
* Disables JUnit's normal timeout mode.
*
* @see TimeLimitedStatement
*/
@Override
protected Statement withPotentialTimeout(FrameworkMethod method, Object test, Statement next) {
return next;
}
private long getTimeout(Test annotation) {
if (annotation == null) {
return 0;
}
return annotation.timeout();
}
}
@Nonnull
protected Class<?>[] getExtraShadows(FrameworkMethod method) {
List<Class<?>> shadowClasses = new ArrayList<>();
addShadows(shadowClasses, getTestClass().getJavaClass().getAnnotation(SandboxConfig.class));
addShadows(shadowClasses, method.getAnnotation(SandboxConfig.class));
return shadowClasses.toArray(new Class[shadowClasses.size()]);
}
private void addShadows(List<Class<?>> shadowClasses, SandboxConfig annotation) {
if (annotation != null) {
shadowClasses.addAll(asList(annotation.shadows()));
}
}
protected ShadowMap createShadowMap() {
return BASE_SHADOW_MAP;
}
@Nonnull
protected ClassHandler createClassHandler(ShadowMap shadowMap, Sandbox sandbox) {
return new ShadowWrangler(shadowMap, 0, interceptors);
}
/**
* Disables JUnit's normal timeout mode.
*
* @see TimeLimitedStatement
*/
protected Statement withPotentialTimeout(FrameworkMethod method, Object test, Statement next) {
return next;
}
}