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 }