Home | History | Annotate | Download | only in internal
      1 package org.robolectric.internal;
      2 
      3 import static java.util.Arrays.asList;
      4 
      5 import com.google.common.collect.Lists;
      6 import java.lang.reflect.Method;
      7 import java.net.URLClassLoader;
      8 import java.util.ArrayList;
      9 import java.util.Collection;
     10 import java.util.Collections;
     11 import java.util.HashSet;
     12 import java.util.List;
     13 import java.util.ServiceLoader;
     14 import javax.annotation.Nonnull;
     15 import org.junit.AfterClass;
     16 import org.junit.BeforeClass;
     17 import org.junit.Ignore;
     18 import org.junit.internal.AssumptionViolatedException;
     19 import org.junit.internal.runners.model.EachTestNotifier;
     20 import org.junit.runner.Description;
     21 import org.junit.runner.notification.RunNotifier;
     22 import org.junit.runners.BlockJUnit4ClassRunner;
     23 import org.junit.runners.model.FrameworkMethod;
     24 import org.junit.runners.model.InitializationError;
     25 import org.junit.runners.model.Statement;
     26 import org.junit.runners.model.TestClass;
     27 import org.robolectric.internal.bytecode.ClassHandler;
     28 import org.robolectric.internal.bytecode.InstrumentationConfiguration;
     29 import org.robolectric.internal.bytecode.Interceptor;
     30 import org.robolectric.internal.bytecode.Interceptors;
     31 import org.robolectric.internal.bytecode.Sandbox;
     32 import org.robolectric.internal.bytecode.SandboxClassLoader;
     33 import org.robolectric.internal.bytecode.SandboxConfig;
     34 import org.robolectric.internal.bytecode.ShadowMap;
     35 import org.robolectric.internal.bytecode.ShadowWrangler;
     36 import org.robolectric.util.PerfStatsCollector;
     37 import org.robolectric.util.PerfStatsCollector.Event;
     38 import org.robolectric.util.PerfStatsCollector.Metadata;
     39 import org.robolectric.util.PerfStatsCollector.Metric;
     40 import org.robolectric.util.PerfStatsReporter;
     41 
     42 public class SandboxTestRunner extends BlockJUnit4ClassRunner {
     43 
     44   private final Interceptors interceptors;
     45   private final List<PerfStatsReporter> perfStatsReporters;
     46   private final HashSet<Class<?>> loadedTestClasses = new HashSet<>();
     47 
     48   public SandboxTestRunner(Class<?> klass) throws InitializationError {
     49     super(klass);
     50 
     51     interceptors = new Interceptors(findInterceptors());
     52     perfStatsReporters = Lists.newArrayList(getPerfStatsReporters().iterator());
     53   }
     54 
     55   @Nonnull
     56   protected Iterable<PerfStatsReporter> getPerfStatsReporters() {
     57     return ServiceLoader.load(PerfStatsReporter.class);
     58   }
     59 
     60   @Nonnull
     61   protected Collection<Interceptor> findInterceptors() {
     62     return Collections.emptyList();
     63   }
     64 
     65   @Nonnull
     66   protected Interceptors getInterceptors() {
     67     return interceptors;
     68   }
     69 
     70   @Override
     71   protected Statement classBlock(RunNotifier notifier) {
     72     final Statement statement = childrenInvoker(notifier);
     73     return new Statement() {
     74       @Override
     75       public void evaluate() throws Throwable {
     76         try {
     77           statement.evaluate();
     78           for (Class<?> testClass : loadedTestClasses) {
     79             invokeAfterClass(testClass);
     80           }
     81         } finally {
     82           afterClass();
     83           loadedTestClasses.clear();
     84         }
     85       }
     86     };
     87   }
     88 
     89   private void invokeBeforeClass(final Class clazz) throws Throwable {
     90     if (!loadedTestClasses.contains(clazz)) {
     91       loadedTestClasses.add(clazz);
     92 
     93       final TestClass testClass = new TestClass(clazz);
     94       final List<FrameworkMethod> befores = testClass.getAnnotatedMethods(BeforeClass.class);
     95       for (FrameworkMethod before : befores) {
     96         before.invokeExplosively(null);
     97       }
     98     }
     99   }
    100 
    101   private static void invokeAfterClass(final Class<?> clazz) throws Throwable {
    102     final TestClass testClass = new TestClass(clazz);
    103     final List<FrameworkMethod> afters = testClass.getAnnotatedMethods(AfterClass.class);
    104     for (FrameworkMethod after : afters) {
    105       after.invokeExplosively(null);
    106     }
    107   }
    108 
    109   protected void afterClass() {
    110   }
    111 
    112   @Override
    113   protected void runChild(FrameworkMethod method, RunNotifier notifier) {
    114     Description description = describeChild(method);
    115     EachTestNotifier eachNotifier = new EachTestNotifier(notifier, description);
    116 
    117     if (shouldIgnore(method)) {
    118       eachNotifier.fireTestIgnored();
    119     } else {
    120       eachNotifier.fireTestStarted();
    121 
    122       try {
    123         methodBlock(method).evaluate();
    124       } catch (AssumptionViolatedException e) {
    125         eachNotifier.addFailedAssumption(e);
    126       } catch (Throwable e) {
    127         eachNotifier.addFailure(e);
    128       } finally {
    129         eachNotifier.fireTestFinished();
    130       }
    131     }
    132   }
    133 
    134   @Nonnull
    135   protected Sandbox getSandbox(FrameworkMethod method) {
    136     InstrumentationConfiguration instrumentationConfiguration = createClassLoaderConfig(method);
    137     ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
    138     ClassLoader sandboxClassLoader = new SandboxClassLoader(systemClassLoader, instrumentationConfiguration);
    139     Sandbox sandbox = new Sandbox(sandboxClassLoader);
    140     configureShadows(method, sandbox);
    141     return sandbox;
    142   }
    143 
    144   /**
    145    * Create an {@link InstrumentationConfiguration} suitable for the provided {@link FrameworkMethod}.
    146    *
    147    * Custom TestRunner subclasses may wish to override this method to provide alternate configuration.
    148    *
    149    * @param method the test method that's about to run
    150    * @return an {@link InstrumentationConfiguration}
    151    */
    152   @Nonnull
    153   protected InstrumentationConfiguration createClassLoaderConfig(FrameworkMethod method) {
    154     InstrumentationConfiguration.Builder builder = InstrumentationConfiguration.newBuilder()
    155         .doNotAcquirePackage("java.")
    156         .doNotAcquirePackage("sun.")
    157         .doNotAcquirePackage("org.robolectric.annotation.")
    158         .doNotAcquirePackage("org.robolectric.internal.")
    159         .doNotAcquirePackage("org.robolectric.util.")
    160         .doNotAcquirePackage("org.junit.");
    161 
    162     for (Class<?> shadowClass : getExtraShadows(method)) {
    163       ShadowMap.ShadowInfo shadowInfo = ShadowMap.getShadowInfo(shadowClass);
    164       builder.addInstrumentedClass(shadowInfo.getShadowedClassName());
    165     }
    166 
    167     addInstrumentedPackages(method, builder);
    168 
    169     return builder.build();
    170   }
    171 
    172   private void addInstrumentedPackages(FrameworkMethod method, InstrumentationConfiguration.Builder builder) {
    173     SandboxConfig classConfig = getTestClass().getJavaClass().getAnnotation(SandboxConfig.class);
    174     if (classConfig != null) {
    175       for (String pkgName : classConfig.instrumentedPackages()) {
    176         builder.addInstrumentedPackage(pkgName);
    177       }
    178     }
    179 
    180     SandboxConfig methodConfig = method.getAnnotation(SandboxConfig.class);
    181     if (methodConfig != null) {
    182       for (String pkgName : methodConfig.instrumentedPackages()) {
    183         builder.addInstrumentedPackage(pkgName);
    184       }
    185     }
    186   }
    187 
    188   protected void configureShadows(FrameworkMethod method, Sandbox sandbox) {
    189     ShadowMap.Builder builder = createShadowMap().newBuilder();
    190 
    191     // Configure shadows *BEFORE* setting the ClassLoader. This is necessary because
    192     // creating the ShadowMap loads all ShadowProviders via ServiceLoader and this is
    193     // not available once we install the Robolectric class loader.
    194     Class<?>[] shadows = getExtraShadows(method);
    195     if (shadows.length > 0) {
    196       builder.addShadowClasses(shadows);
    197     }
    198     ShadowMap shadowMap = builder.build();
    199     sandbox.replaceShadowMap(shadowMap);
    200 
    201     sandbox.configure(createClassHandler(shadowMap, sandbox), getInterceptors());
    202   }
    203 
    204   @Override protected Statement methodBlock(final FrameworkMethod method) {
    205     return new Statement() {
    206       @Override
    207       public void evaluate() throws Throwable {
    208         PerfStatsCollector perfStatsCollector = PerfStatsCollector.getInstance();
    209         perfStatsCollector.reset();
    210         perfStatsCollector.setEnabled(!perfStatsReporters.isEmpty());
    211 
    212         Event initialization = perfStatsCollector.startEvent("initialization");
    213 
    214         Sandbox sandbox = getSandbox(method);
    215 
    216         // Configure shadows *BEFORE* setting the ClassLoader. This is necessary because
    217         // creating the ShadowMap loads all ShadowProviders via ServiceLoader and this is
    218         // not available once we install the Robolectric class loader.
    219         configureShadows(method, sandbox);
    220 
    221         final ClassLoader priorContextClassLoader = Thread.currentThread().getContextClassLoader();
    222         Thread.currentThread().setContextClassLoader(sandbox.getRobolectricClassLoader());
    223 
    224         //noinspection unchecked
    225         Class bootstrappedTestClass = sandbox.bootstrappedClass(getTestClass().getJavaClass());
    226         HelperTestRunner helperTestRunner = getHelperTestRunner(bootstrappedTestClass);
    227         helperTestRunner.frameworkMethod = method;
    228 
    229         final Method bootstrappedMethod;
    230         try {
    231           //noinspection unchecked
    232           bootstrappedMethod = bootstrappedTestClass.getMethod(method.getMethod().getName());
    233         } catch (NoSuchMethodException e) {
    234           throw new RuntimeException(e);
    235         }
    236 
    237         try {
    238           // Only invoke @BeforeClass once per class
    239           invokeBeforeClass(bootstrappedTestClass);
    240 
    241           beforeTest(sandbox, method, bootstrappedMethod);
    242 
    243           initialization.finished();
    244 
    245           final Statement statement = helperTestRunner.methodBlock(new FrameworkMethod(bootstrappedMethod));
    246 
    247           // todo: this try/finally probably isn't right -- should mimic RunAfters? [xw]
    248           try {
    249             statement.evaluate();
    250           } finally {
    251             afterTest(method, bootstrappedMethod);
    252           }
    253         } finally {
    254           Thread.currentThread().setContextClassLoader(priorContextClassLoader);
    255           finallyAfterTest(method);
    256 
    257           reportPerfStats(perfStatsCollector);
    258           perfStatsCollector.reset();
    259         }
    260       }
    261     };
    262   }
    263 
    264   private void reportPerfStats(PerfStatsCollector perfStatsCollector) {
    265     if (perfStatsReporters.isEmpty()) {
    266       return;
    267     }
    268 
    269     Metadata metadata = perfStatsCollector.getMetadata();
    270     Collection<Metric> metrics = perfStatsCollector.getMetrics();
    271 
    272     for (PerfStatsReporter perfStatsReporter : perfStatsReporters) {
    273       try {
    274         perfStatsReporter.report(metadata, metrics);
    275       } catch (Exception e) {
    276         e.printStackTrace();
    277       }
    278     }
    279   }
    280 
    281   protected void beforeTest(Sandbox sandbox, FrameworkMethod method, Method bootstrappedMethod) throws Throwable {
    282   }
    283 
    284   protected void afterTest(FrameworkMethod method, Method bootstrappedMethod) {
    285   }
    286 
    287   protected void finallyAfterTest(FrameworkMethod method) {
    288   }
    289 
    290   protected HelperTestRunner getHelperTestRunner(Class bootstrappedTestClass) {
    291     try {
    292       return new HelperTestRunner(bootstrappedTestClass);
    293     } catch (InitializationError initializationError) {
    294       throw new RuntimeException(initializationError);
    295     }
    296   }
    297 
    298   protected static class HelperTestRunner extends BlockJUnit4ClassRunner {
    299     public FrameworkMethod frameworkMethod;
    300 
    301     public HelperTestRunner(Class<?> klass) throws InitializationError {
    302       super(klass);
    303     }
    304 
    305     // cuz accessibility
    306     @Override
    307     protected Statement methodBlock(FrameworkMethod method) {
    308       return super.methodBlock(method);
    309     }
    310   }
    311 
    312   @Nonnull
    313   protected Class<?>[] getExtraShadows(FrameworkMethod method) {
    314     List<Class<?>> shadowClasses = new ArrayList<>();
    315     addShadows(shadowClasses, getTestClass().getJavaClass().getAnnotation(SandboxConfig.class));
    316     addShadows(shadowClasses, method.getAnnotation(SandboxConfig.class));
    317     return shadowClasses.toArray(new Class[shadowClasses.size()]);
    318   }
    319 
    320   private void addShadows(List<Class<?>> shadowClasses, SandboxConfig annotation) {
    321     if (annotation != null) {
    322       shadowClasses.addAll(asList(annotation.shadows()));
    323     }
    324   }
    325 
    326   protected ShadowMap createShadowMap() {
    327     return ShadowMap.EMPTY;
    328   }
    329 
    330   @Nonnull
    331   protected ClassHandler createClassHandler(ShadowMap shadowMap, Sandbox sandbox) {
    332     return new ShadowWrangler(shadowMap, 0, interceptors);
    333   }
    334 
    335   protected boolean shouldIgnore(FrameworkMethod method) {
    336     return method.getAnnotation(Ignore.class) != null;
    337   }
    338 }