1package org.robolectric; 2 3import android.app.Application; 4import com.google.common.annotations.VisibleForTesting; 5import com.google.common.collect.ImmutableMap; 6import java.io.File; 7import java.io.IOException; 8import java.io.InputStream; 9import java.lang.reflect.Constructor; 10import java.lang.reflect.InvocationTargetException; 11import java.lang.reflect.Method; 12import java.net.URL; 13import java.security.SecureRandom; 14import java.util.ArrayList; 15import java.util.Collection; 16import java.util.HashMap; 17import java.util.List; 18import java.util.Map; 19import java.util.Properties; 20import java.util.ServiceLoader; 21import javax.annotation.Nonnull; 22import org.junit.Ignore; 23import org.junit.runners.model.FrameworkMethod; 24import org.junit.runners.model.InitializationError; 25import org.junit.runners.model.Statement; 26import org.robolectric.android.AndroidInterceptors; 27import org.robolectric.android.internal.ParallelUniverse; 28import org.robolectric.annotation.Config; 29import org.robolectric.internal.AndroidConfigurer; 30import org.robolectric.internal.BuckManifestFactory; 31import org.robolectric.internal.DefaultManifestFactory; 32import org.robolectric.internal.GradleManifestFactory; 33import org.robolectric.internal.ManifestFactory; 34import org.robolectric.internal.ManifestIdentifier; 35import org.robolectric.internal.MavenManifestFactory; 36import org.robolectric.internal.ParallelUniverseInterface; 37import org.robolectric.internal.SandboxFactory; 38import org.robolectric.internal.SandboxTestRunner; 39import org.robolectric.internal.SdkConfig; 40import org.robolectric.internal.SdkEnvironment; 41import org.robolectric.internal.ShadowProvider; 42import org.robolectric.internal.bytecode.ClassHandler; 43import org.robolectric.internal.bytecode.InstrumentationConfiguration; 44import org.robolectric.internal.bytecode.InstrumentationConfiguration.Builder; 45import org.robolectric.internal.bytecode.Interceptor; 46import org.robolectric.internal.bytecode.Sandbox; 47import org.robolectric.internal.bytecode.SandboxClassLoader; 48import org.robolectric.internal.bytecode.ShadowMap; 49import org.robolectric.internal.bytecode.ShadowWrangler; 50import org.robolectric.internal.dependency.CachedDependencyResolver; 51import org.robolectric.internal.dependency.DependencyResolver; 52import org.robolectric.internal.dependency.LocalDependencyResolver; 53import org.robolectric.internal.dependency.PropertiesDependencyResolver; 54import org.robolectric.manifest.AndroidManifest; 55import org.robolectric.res.Fs; 56import org.robolectric.res.FsFile; 57import org.robolectric.res.PackageResourceTable; 58import org.robolectric.res.ResourceMerger; 59import org.robolectric.res.ResourcePath; 60import org.robolectric.res.ResourceTable; 61import org.robolectric.res.ResourceTableFactory; 62import org.robolectric.res.RoutingResourceTable; 63import org.robolectric.util.Logger; 64import org.robolectric.util.PerfStatsCollector; 65import org.robolectric.util.ReflectionHelpers; 66 67/** 68 * Installs a {@link SandboxClassLoader} and {@link ResourceTable} in order to 69 * provide a simulation of the Android runtime environment. 70 */ 71public class RobolectricTestRunner extends SandboxTestRunner { 72 73 public static final String CONFIG_PROPERTIES = "robolectric.properties"; 74 75 private static final Map<AndroidManifest, PackageResourceTable> appResourceTableCache = new HashMap<>(); 76 private static final Map<ManifestIdentifier, AndroidManifest> appManifestsCache = new HashMap<>(); 77 private static PackageResourceTable compiletimeSdkResourceTable; 78 79 private final SdkPicker sdkPicker; 80 private final ConfigMerger configMerger; 81 private ServiceLoader<ShadowProvider> providers; 82 private transient DependencyResolver dependencyResolver; 83 84 static { 85 new SecureRandom(); // this starts up the Poller SunPKCS11-Darwin thread early, outside of any Robolectric classloader 86 } 87 88 /** 89 * Creates a runner to run {@code testClass}. Looks in your working directory for your AndroidManifest.xml file 90 * and res directory by default. Use the {@link Config} annotation to configure. 91 * 92 * @param testClass the test class to be run 93 * @throws InitializationError if junit says so 94 */ 95 public RobolectricTestRunner(final Class<?> testClass) throws InitializationError { 96 super(testClass); 97 this.configMerger = createConfigMerger(); 98 this.sdkPicker = createSdkPicker(); 99 } 100 101 protected DependencyResolver getJarResolver() { 102 if (dependencyResolver == null) { 103 if (Boolean.getBoolean("robolectric.offline")) { 104 String propPath = System.getProperty("robolectric-deps.properties"); 105 if (propPath != null) { 106 try { 107 dependencyResolver = new PropertiesDependencyResolver( 108 Fs.newFile(propPath), 109 null); 110 } catch (IOException e) { 111 throw new RuntimeException("couldn't read dependencies" , e); 112 } 113 } else { 114 String dependencyDir = System.getProperty("robolectric.dependency.dir", "."); 115 dependencyResolver = new LocalDependencyResolver(new File(dependencyDir)); 116 } 117 } else { 118 File cacheDir = new File(new File(System.getProperty("java.io.tmpdir")), "robolectric"); 119 120 Class<?> mavenDependencyResolverClass = ReflectionHelpers.loadClass(RobolectricTestRunner.class.getClassLoader(), 121 "org.robolectric.internal.dependency.MavenDependencyResolver"); 122 DependencyResolver dependencyResolver = (DependencyResolver) ReflectionHelpers.callConstructor(mavenDependencyResolverClass); 123 if (cacheDir.exists() || cacheDir.mkdir()) { 124 Logger.info("Dependency cache location: %s", cacheDir.getAbsolutePath()); 125 this.dependencyResolver = new CachedDependencyResolver(dependencyResolver, cacheDir, 60 * 60 * 24 * 1000); 126 } else { 127 this.dependencyResolver = dependencyResolver; 128 } 129 } 130 131 URL buildPathPropertiesUrl = getClass().getClassLoader().getResource("robolectric-deps.properties"); 132 if (buildPathPropertiesUrl != null) { 133 Logger.info("Using Robolectric classes from %s", buildPathPropertiesUrl.getPath()); 134 135 FsFile propertiesFile = Fs.fileFromPath(buildPathPropertiesUrl.getFile()); 136 try { 137 dependencyResolver = new PropertiesDependencyResolver(propertiesFile, dependencyResolver); 138 } catch (IOException e) { 139 throw new RuntimeException("couldn't read " + buildPathPropertiesUrl, e); 140 } 141 } 142 } 143 144 return dependencyResolver; 145 } 146 147 /** 148 * Create a {@link ClassHandler} appropriate for the given arguments. 149 * 150 * Robolectric may chose to cache the returned instance, keyed by <tt>shadowMap</tt> and <tt>sdkConfig</tt>. 151 * 152 * Custom TestRunner subclasses may wish to override this method to provide alternate configuration. 153 * 154 * @param shadowMap the {@link ShadowMap} in effect for this test 155 * @param sandbox the {@link SdkConfig} in effect for this test 156 * @return an appropriate {@link ClassHandler}. This implementation returns a {@link ShadowWrangler}. 157 * @since 2.3 158 */ 159 @Override 160 @Nonnull 161 protected ClassHandler createClassHandler(ShadowMap shadowMap, Sandbox sandbox) { 162 return new ShadowWrangler(shadowMap, ((SdkEnvironment) sandbox).getSdkConfig().getApiLevel(), getInterceptors()); 163 } 164 165 /** 166 * Create a {@link ConfigMerger} for calculating the {@link Config} tests. 167 * 168 * Custom TestRunner subclasses may wish to override this method to provide alternate configuration. 169 * 170 * @return an {@link ConfigMerger}. 171 * @since 3.2 172 */ 173 @Nonnull 174 private ConfigMerger createConfigMerger() { 175 return new ConfigMerger(); 176 } 177 178 /** 179 * Create a {@link SdkPicker} for determining which SDKs will be tested. 180 * 181 * Custom TestRunner subclasses may wish to override this method to provide alternate configuration. 182 * 183 * @return an {@link SdkPicker}. 184 * @since 3.2 185 */ 186 @Nonnull 187 protected SdkPicker createSdkPicker() { 188 return new SdkPicker(); 189 } 190 191 @Override 192 @Nonnull // todo 193 protected Collection<Interceptor> findInterceptors() { 194 return AndroidInterceptors.all(); 195 } 196 197 /** 198 * Create an {@link InstrumentationConfiguration} suitable for the provided {@link Config}. 199 * 200 * Custom TestRunner subclasses may wish to override this method to provide alternate configuration. 201 * 202 * @param config the merged configuration for the test that's about to run -- todo 203 * @return an {@link InstrumentationConfiguration} 204 * @deprecated Override {@link #createClassLoaderConfig(FrameworkMethod)} instead 205 */ 206 @Deprecated 207 @Nonnull 208 public InstrumentationConfiguration createClassLoaderConfig(Config config) { 209 FrameworkMethod method = ((MethodPassThrough) config).method; 210 Builder builder = new InstrumentationConfiguration.Builder(super.createClassLoaderConfig(method)); 211 AndroidConfigurer.configure(builder, getInterceptors()); 212 AndroidConfigurer.withConfig(builder, config); 213 return builder.build(); 214 } 215 216 /** 217 * {@inheritDoc} 218 */ 219 @Override @Nonnull 220 protected InstrumentationConfiguration createClassLoaderConfig(final FrameworkMethod method) { 221 return createClassLoaderConfig(new Config.Builder(((RobolectricFrameworkMethod) method).config) { 222 @Override 223 public Config.Implementation build() { 224 return new MethodPassThrough(method, sdk, minSdk, maxSdk, manifest, qualifiers, packageName, abiSplit, resourceDir, assetDir, buildDir, shadows, instrumentedPackages, application, libraries, constants); 225 } 226 }.build()); 227 } 228 229 /** 230 * An instance of the returned class will be created for each test invocation. 231 * 232 * Custom TestRunner subclasses may wish to override this method to provide alternate configuration. 233 * 234 * @return a class which implements {@link TestLifecycle}. This implementation returns a {@link DefaultTestLifecycle}. 235 */ 236 @Nonnull 237 protected Class<? extends TestLifecycle> getTestLifecycleClass() { 238 return DefaultTestLifecycle.class; 239 } 240 241 @Override 242 protected List<FrameworkMethod> getChildren() { 243 List<FrameworkMethod> children = new ArrayList<>(); 244 for (FrameworkMethod frameworkMethod : super.getChildren()) { 245 try { 246 Config config = getConfig(frameworkMethod.getMethod()); 247 AndroidManifest appManifest = getAppManifest(config); 248 249 List<SdkConfig> sdksToRun = sdkPicker.selectSdks(config, appManifest); 250 RobolectricFrameworkMethod last = null; 251 for (SdkConfig sdkConfig : sdksToRun) { 252 last = new RobolectricFrameworkMethod(frameworkMethod.getMethod(), appManifest, sdkConfig, config); 253 children.add(last); 254 } 255 if (last != null) { 256 last.dontIncludeApiLevelInName(); 257 } 258 } catch (IllegalArgumentException e) { 259 throw new IllegalArgumentException("failed to configure " + 260 getTestClass().getName() + "." + frameworkMethod.getMethod().getName() + 261 ": " + e.getMessage(), e); 262 } 263 } 264 return children; 265 } 266 267 /** 268 * Returns the ResourceProvider for the compile time SDK. 269 */ 270 @Nonnull 271 private static PackageResourceTable getCompiletimeSdkResourceTable() { 272 if (compiletimeSdkResourceTable == null) { 273 ResourceTableFactory resourceTableFactory = new ResourceTableFactory(); 274 compiletimeSdkResourceTable = resourceTableFactory.newFrameworkResourceTable(new ResourcePath(android.R.class, null, null)); 275 } 276 return compiletimeSdkResourceTable; 277 } 278 279 /** 280 * @deprecated Override {@link #shouldIgnore(FrameworkMethod)} instead. 281 */ 282 @Deprecated 283 protected boolean shouldIgnore(FrameworkMethod method, Config config) { 284 return method.getAnnotation(Ignore.class) != null; 285 } 286 287 @Override protected boolean shouldIgnore(FrameworkMethod method) { 288 return shouldIgnore(method, ((RobolectricFrameworkMethod) method).config); 289 } 290 291 @Override 292 @Nonnull 293 protected SdkEnvironment getSandbox(FrameworkMethod method) { 294 RobolectricFrameworkMethod roboMethod = (RobolectricFrameworkMethod) method; 295 SdkConfig sdkConfig = roboMethod.sdkConfig; 296 return SandboxFactory.INSTANCE.getSdkEnvironment( 297 createClassLoaderConfig(method), getJarResolver(), sdkConfig); 298 } 299 300 @Override 301 protected void beforeTest(Sandbox sandbox, FrameworkMethod method, Method bootstrappedMethod) throws Throwable { 302 SdkEnvironment sdkEnvironment = (SdkEnvironment) sandbox; 303 RobolectricFrameworkMethod roboMethod = (RobolectricFrameworkMethod) method; 304 305 PerfStatsCollector perfStatsCollector = PerfStatsCollector.getInstance(); 306 SdkConfig sdkConfig = roboMethod.sdkConfig; 307 perfStatsCollector.putMetadata(AndroidMetadata.class, 308 new AndroidMetadata( 309 ImmutableMap.of("ro.build.version.sdk", "" + sdkConfig.getApiLevel()))); 310 311 roboMethod.parallelUniverseInterface = getHooksInterface(sdkEnvironment); 312 Class<TestLifecycle> cl = sdkEnvironment.bootstrappedClass(getTestLifecycleClass()); 313 roboMethod.testLifecycle = ReflectionHelpers.newInstance(cl); 314 315 providers = ServiceLoader.load(ShadowProvider.class, sdkEnvironment.getRobolectricClassLoader()); 316 317 roboMethod.parallelUniverseInterface.setSdkConfig(sdkConfig); 318 perfStatsCollector.measure("reset Android state (before test)", 319 () -> resetStaticState()); 320 321 AndroidManifest appManifest = roboMethod.getAppManifest(); 322 PackageResourceTable systemResourceTable = sdkEnvironment.getSystemResourceTable(getJarResolver()); 323 PackageResourceTable appResourceTable = getAppResourceTable(appManifest); 324 325 roboMethod.parallelUniverseInterface.setUpApplicationState( 326 bootstrappedMethod, 327 appManifest, 328 roboMethod.config, 329 new RoutingResourceTable(appResourceTable, getCompiletimeSdkResourceTable()), 330 new RoutingResourceTable(appResourceTable, systemResourceTable), 331 new RoutingResourceTable(systemResourceTable)); 332 roboMethod.testLifecycle.beforeTest(bootstrappedMethod); 333 } 334 335 @Override 336 protected void afterTest(FrameworkMethod method, Method bootstrappedMethod) { 337 RobolectricFrameworkMethod roboMethod = (RobolectricFrameworkMethod) method; 338 339 try { 340 roboMethod.parallelUniverseInterface.tearDownApplication(); 341 } finally { 342 try { 343 internalAfterTest(method, bootstrappedMethod); 344 } finally { 345 // reset static state afterward too, so statics don't defeat GC? 346 PerfStatsCollector.getInstance().measure("reset Android state (after test)", 347 () -> resetStaticState()); 348 } 349 } 350 } 351 352 private void resetStaticState() { 353 for (ShadowProvider provider : providers) { 354 provider.reset(); 355 } 356 } 357 358 @Override 359 protected void finallyAfterTest(FrameworkMethod method) { 360 RobolectricFrameworkMethod roboMethod = (RobolectricFrameworkMethod) method; 361 362 roboMethod.testLifecycle = null; 363 roboMethod.parallelUniverseInterface = null; 364 } 365 366 @Override protected SandboxTestRunner.HelperTestRunner getHelperTestRunner(Class bootstrappedTestClass) { 367 try { 368 return new HelperTestRunner(bootstrappedTestClass); 369 } catch (InitializationError initializationError) { 370 throw new RuntimeException(initializationError); 371 } 372 } 373 374 /** 375 * Detects which build system is in use and returns the appropriate ManifestFactory implementation. 376 * 377 * Custom TestRunner subclasses may wish to override this method to provide alternate configuration. 378 * 379 * @param config Specification of the SDK version, manifest file, package name, etc. 380 */ 381 protected ManifestFactory getManifestFactory(Config config) { 382 Properties buildSystemApiProperties = getBuildSystemApiProperties(); 383 if (buildSystemApiProperties != null) { 384 return new DefaultManifestFactory(buildSystemApiProperties); 385 } 386 387 Class<?> buildConstants = config.constants(); 388 //noinspection ConstantConditions 389 if (BuckManifestFactory.isBuck()) { 390 return new BuckManifestFactory(); 391 } else if (buildConstants != null && buildConstants != Void.class) { 392 return new GradleManifestFactory(); 393 } else { 394 return new MavenManifestFactory(); 395 } 396 } 397 398 Properties getBuildSystemApiProperties() { 399 InputStream resourceAsStream = getClass().getResourceAsStream("/com/android/tools/test_config.properties"); 400 if (resourceAsStream == null) { 401 return null; 402 } 403 404 try { 405 Properties properties = new Properties(); 406 properties.load(resourceAsStream); 407 return properties; 408 } catch (IOException e) { 409 return null; 410 } finally { 411 try { 412 resourceAsStream.close(); 413 } catch (IOException e) { 414 // ignore 415 } 416 } 417 } 418 419 protected AndroidManifest getAppManifest(Config config) { 420 ManifestFactory manifestFactory = getManifestFactory(config); 421 ManifestIdentifier identifier = manifestFactory.identify(config); 422 423 synchronized (appManifestsCache) { 424 AndroidManifest appManifest; 425 appManifest = appManifestsCache.get(identifier); 426 if (appManifest == null) { 427 appManifest = createAndroidManifest(identifier); 428 appManifestsCache.put(identifier, appManifest); 429 } 430 431 return appManifest; 432 } 433 } 434 435 /** 436 * Internal use only. 437 */ 438 @VisibleForTesting 439 public static AndroidManifest createAndroidManifest(ManifestIdentifier manifestIdentifier) { 440 List<ManifestIdentifier> libraries = manifestIdentifier.getLibraries(); 441 442 List<AndroidManifest> libraryManifests = new ArrayList<>(); 443 for (ManifestIdentifier library : libraries) { 444 libraryManifests.add(createAndroidManifest(library)); 445 } 446 447 return new AndroidManifest(manifestIdentifier.getManifestFile(), manifestIdentifier.getResDir(), 448 manifestIdentifier.getAssetDir(), libraryManifests, manifestIdentifier.getPackageName()); 449 } 450 451 452 /** 453 * Compute the effective Robolectric configuration for a given test method. 454 * 455 * Configuration information is collected from package-level <tt>robolectric.properties</tt> files 456 * and {@link Config} annotations on test classes, superclasses, and methods. 457 * 458 * Custom TestRunner subclasses may wish to override this method to provide alternate configuration. 459 * 460 * @param method the test method 461 * @return the effective Robolectric configuration for the given test method 462 * @since 2.0 463 */ 464 public Config getConfig(Method method) { 465 return configMerger.getConfig(getTestClass().getJavaClass(), method, buildGlobalConfig()); 466 } 467 468 /** 469 * Provides the base Robolectric configuration {@link Config} used for all tests. 470 * 471 * Configuration provided for specific packages, test classes, and test method 472 * configurations will override values provided here. 473 * 474 * Custom TestRunner subclasses may wish to override this method to provide 475 * alternate configuration. Consider using a {@link Config.Builder}. 476 * 477 * The default implementation has appropriate values for most use cases. 478 * 479 * @return global {@link Config} object 480 * @since 3.1.3 481 */ 482 protected Config buildGlobalConfig() { 483 return new Config.Builder().build(); 484 } 485 486 @Override @Nonnull 487 protected Class<?>[] getExtraShadows(FrameworkMethod frameworkMethod) { 488 Config config = ((RobolectricFrameworkMethod) frameworkMethod).config; 489 return config.shadows(); 490 } 491 492 ParallelUniverseInterface getHooksInterface(SdkEnvironment sdkEnvironment) { 493 ClassLoader robolectricClassLoader = sdkEnvironment.getRobolectricClassLoader(); 494 try { 495 Class<?> clazz = robolectricClassLoader.loadClass(ParallelUniverse.class.getName()); 496 Class<? extends ParallelUniverseInterface> typedClazz = clazz.asSubclass(ParallelUniverseInterface.class); 497 Constructor<? extends ParallelUniverseInterface> constructor = typedClazz.getConstructor(); 498 return constructor.newInstance(); 499 } catch (ClassNotFoundException | NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) { 500 throw new RuntimeException(e); 501 } 502 } 503 504 protected void internalAfterTest(FrameworkMethod frameworkMethod, Method method) { 505 RobolectricFrameworkMethod roboMethod = (RobolectricFrameworkMethod) frameworkMethod; 506 roboMethod.testLifecycle.afterTest(method); 507 } 508 509 @Override 510 protected void afterClass() { 511 } 512 513 @Override 514 public Object createTest() throws Exception { 515 throw new UnsupportedOperationException("this should always be invoked on the HelperTestRunner!"); 516 } 517 518 private PackageResourceTable getAppResourceTable(final AndroidManifest appManifest) { 519 PackageResourceTable resourceTable = appResourceTableCache.get(appManifest); 520 if (resourceTable == null) { 521 resourceTable = new ResourceMerger().buildResourceTable(appManifest); 522 523 appResourceTableCache.put(appManifest, resourceTable); 524 } 525 return resourceTable; 526 } 527 528 @SuppressWarnings(value = {"ImmutableAnnotationChecker", "BadAnnotationImplementation"}) 529 private static class MethodPassThrough extends Config.Implementation { 530 private final FrameworkMethod method; 531 532 private MethodPassThrough(FrameworkMethod method, int[] sdk, int minSdk, int maxSdk, String manifest, String qualifiers, String packageName, String abiSplit, String resourceDir, String assetDir, String buildDir, Class<?>[] shadows, String[] instrumentedPackages, Class<? extends Application> application, String[] libraries, Class<?> constants) { 533 super(sdk, minSdk, maxSdk, manifest, qualifiers, packageName, abiSplit, resourceDir, assetDir, buildDir, shadows, instrumentedPackages, application, libraries, constants); 534 this.method = method; 535 } 536 } 537 538 public static class HelperTestRunner extends SandboxTestRunner.HelperTestRunner { 539 public HelperTestRunner(Class bootstrappedTestClass) throws InitializationError { 540 super(bootstrappedTestClass); 541 } 542 543 @Override protected Object createTest() throws Exception { 544 Object test = super.createTest(); 545 RobolectricFrameworkMethod roboMethod = (RobolectricFrameworkMethod) this.frameworkMethod; 546 roboMethod.testLifecycle.prepareTest(test); 547 return test; 548 } 549 550 @Override 551 protected Statement methodInvoker(FrameworkMethod method, Object test) { 552 final Statement invoker = super.methodInvoker(method, test); 553 final RobolectricFrameworkMethod roboMethod = (RobolectricFrameworkMethod) this.frameworkMethod; 554 return new Statement() { 555 @Override 556 public void evaluate() throws Throwable { 557 Thread orig = roboMethod.parallelUniverseInterface.getMainThread(); 558 roboMethod.parallelUniverseInterface.setMainThread(Thread.currentThread()); 559 try { 560 invoker.evaluate(); 561 } finally { 562 roboMethod.parallelUniverseInterface.setMainThread(orig); 563 } 564 } 565 }; 566 } 567 } 568 569 static class RobolectricFrameworkMethod extends FrameworkMethod { 570 private final @Nonnull AndroidManifest appManifest; 571 final @Nonnull SdkConfig sdkConfig; 572 final @Nonnull Config config; 573 private boolean includeApiLevelInName = true; 574 TestLifecycle testLifecycle; 575 ParallelUniverseInterface parallelUniverseInterface; 576 577 RobolectricFrameworkMethod(@Nonnull Method method, @Nonnull AndroidManifest appManifest, @Nonnull SdkConfig sdkConfig, @Nonnull Config config) { 578 super(method); 579 this.appManifest = appManifest; 580 this.sdkConfig = sdkConfig; 581 this.config = config; 582 } 583 584 @Override 585 public String getName() { 586 // IDE focused test runs rely on preservation of the test name; we'll use the 587 // latest supported SDK for focused test runs 588 return super.getName() + 589 (includeApiLevelInName ? "[" + sdkConfig.getApiLevel() + "]" : ""); 590 } 591 592 void dontIncludeApiLevelInName() { 593 includeApiLevelInName = false; 594 } 595 596 @Nonnull 597 public AndroidManifest getAppManifest() { 598 return appManifest; 599 } 600 601 @Override 602 public boolean equals(Object o) { 603 if (this == o) return true; 604 if (o == null || getClass() != o.getClass()) return false; 605 if (!super.equals(o)) return false; 606 607 RobolectricFrameworkMethod that = (RobolectricFrameworkMethod) o; 608 609 return sdkConfig.equals(that.sdkConfig); 610 } 611 612 @Override 613 public int hashCode() { 614 int result = super.hashCode(); 615 result = 31 * result + sdkConfig.hashCode(); 616 return result; 617 } 618 } 619} 620