package org.robolectric.internal;
import javax.annotation.Nonnull;
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.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.ShadowMap;
import org.robolectric.internal.bytecode.ShadowWrangler;
import java.lang.reflect.Method;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import static java.util.Arrays.asList;
public class SandboxTestRunner extends BlockJUnit4ClassRunner {
private final Interceptors interceptors;
private final HashSet<Class<?>> loadedTestClasses = new HashSet<>();
public SandboxTestRunner(Class<?> klass) throws InitializationError {
super(klass);
interceptors = new Interceptors(findInterceptors());
}
@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() {
}
@Override
protected void runChild(FrameworkMethod method, RunNotifier notifier) {
Description description = describeChild(method);
EachTestNotifier eachNotifier = new EachTestNotifier(notifier, description);
if (shouldIgnore(method)) {
eachNotifier.fireTestIgnored();
} else {
eachNotifier.fireTestStarted();
try {
methodBlock(method).evaluate();
} catch (AssumptionViolatedException e) {
eachNotifier.addFailedAssumption(e);
} catch (Throwable e) {
eachNotifier.addFailure(e);
} finally {
eachNotifier.fireTestFinished();
}
}
}
@Nonnull
protected Sandbox getSandbox(FrameworkMethod method) {
InstrumentationConfiguration instrumentationConfiguration = createClassLoaderConfig(method);
URLClassLoader systemClassLoader = (URLClassLoader) ClassLoader.getSystemClassLoader();
ClassLoader sandboxClassLoader = new SandboxClassLoader(systemClassLoader, instrumentationConfiguration);
Sandbox sandbox = new Sandbox(sandboxClassLoader);
configureShadows(method, sandbox);
return sandbox;
}
/**
* 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.util.")
.doNotAcquirePackage("org.junit.");
for (Class<?> shadowClass : getExtraShadows(method)) {
ShadowMap.ShadowInfo shadowInfo = ShadowMap.getShadowInfo(shadowClass);
builder.addInstrumentedClass(shadowInfo.getShadowedClassName());
}
return builder.build();
}
protected void configureShadows(FrameworkMethod method, Sandbox sandbox) {
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());
}
protected Statement methodBlock(final FrameworkMethod method) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
Sandbox sandbox = getSandbox(method);
// 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(method, sandbox);
final ClassLoader priorContextClassLoader = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(sandbox.getRobolectricClassLoader());
//noinspection unchecked
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);
final 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);
}
} finally {
Thread.currentThread().setContextClassLoader(priorContextClassLoader);
finallyAfterTest(method);
}
}
};
}
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);
}
// cuz accessibility
@Override
protected Statement methodBlock(FrameworkMethod method) {
return super.methodBlock(method);
}
}
@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 ShadowMap.EMPTY;
}
@Nonnull
protected ClassHandler createClassHandler(ShadowMap shadowMap, Sandbox sandbox) {
return new ShadowWrangler(shadowMap, 0, interceptors);
}
protected boolean shouldIgnore(FrameworkMethod method) {
return method.getAnnotation(Ignore.class) != null;
}
}