| package org.robolectric.internal; |
| |
| import static java.util.Arrays.asList; |
| |
| import com.google.common.collect.Lists; |
| import java.lang.reflect.Method; |
| import java.util.ArrayList; |
| 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.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.ShadowInfo; |
| import org.robolectric.internal.bytecode.ShadowMap; |
| import org.robolectric.internal.bytecode.ShadowWrangler; |
| import org.robolectric.util.PerfStatsCollector; |
| import org.robolectric.util.PerfStatsCollector.Event; |
| import org.robolectric.util.PerfStatsCollector.Metadata; |
| import org.robolectric.util.PerfStatsCollector.Metric; |
| import org.robolectric.util.PerfStatsReporter; |
| |
| public class SandboxTestRunner extends BlockJUnit4ClassRunner { |
| |
| private static final ShadowMap BASE_SHADOW_MAP; |
| |
| static { |
| ServiceLoader<ShadowProvider> shadowProviders = ServiceLoader.load(ShadowProvider.class); |
| BASE_SHADOW_MAP = ShadowMap.createFromShadowProviders(shadowProviders); |
| } |
| |
| private final Interceptors interceptors; |
| private final List<PerfStatsReporter> perfStatsReporters; |
| private final HashSet<Class<?>> loadedTestClasses = new HashSet<>(); |
| |
| public SandboxTestRunner(Class<?> klass) throws InitializationError { |
| super(klass); |
| |
| interceptors = new Interceptors(findInterceptors()); |
| perfStatsReporters = Lists.newArrayList(getPerfStatsReporters().iterator()); |
| } |
| |
| @Nonnull |
| protected Iterable<PerfStatsReporter> getPerfStatsReporters() { |
| return ServiceLoader.load(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() { |
| } |
| |
| @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); |
| 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.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); |
| |
| 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); |
| |
| initialization.finished(); |
| |
| 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); |
| |
| 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); |
| } |
| |
| // 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 BASE_SHADOW_MAP; |
| } |
| |
| @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; |
| } |
| } |