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 
SandboxTestRunner(Class<?> klass)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
getPerfStatsReporters()56   protected Iterable<PerfStatsReporter> getPerfStatsReporters() {
57     return ServiceLoader.load(PerfStatsReporter.class);
58   }
59 
60   @Nonnull
findInterceptors()61   protected Collection<Interceptor> findInterceptors() {
62     return Collections.emptyList();
63   }
64 
65   @Nonnull
getInterceptors()66   protected Interceptors getInterceptors() {
67     return interceptors;
68   }
69 
70   @Override
classBlock(RunNotifier notifier)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 }