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