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