blob: 5797a54b2926394522bed6eb2c3f0728362c06dd [file] [log] [blame]
package org.robolectric.internal.bytecode;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.TestLifecycle;
import org.robolectric.annotation.Config;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.annotation.internal.DoNotInstrument;
import org.robolectric.annotation.internal.Instrument;
import org.robolectric.internal.*;
import org.robolectric.internal.dependency.DependencyJar;
import org.robolectric.internal.fakes.RoboCharsets;
import org.robolectric.internal.fakes.RoboExtendedResponseCache;
import org.robolectric.internal.fakes.RoboResponseSource;
import org.robolectric.manifest.AndroidManifest;
import org.robolectric.res.ResourcePath;
import org.robolectric.res.ResourceTable;
import org.robolectric.res.builder.XmlBlock;
import org.robolectric.util.TempDirectory;
import org.robolectric.util.Transcript;
import java.util.*;
/**
* Configuration rules for {@link org.robolectric.internal.bytecode.InstrumentingClassLoader}.
*/
public class InstrumentationConfiguration {
public static final class Builder {
private final Collection<String> instrumentedPackages = new HashSet<>();
private final Collection<MethodRef> interceptedMethods = new HashSet<>();
private final Map<String, String> classNameTranslations = new HashMap<>();
private final Collection<String> classesToNotAcquire = new HashSet<>();
private final Collection<String> packagesToNotAcquire = new HashSet<>();
private final Collection<String> instrumentedClasses = new HashSet<>();
private final Collection<String> classesToNotInstrument = new HashSet<>();
public Builder doNotAcquireClass(String className) {
this.classesToNotAcquire.add(className);
return this;
}
public Builder doNotAcquirePackage(String packageName) {
this.packagesToNotAcquire.add(packageName);
return this;
}
public Builder addClassNameTranslation(String fromName, String toName) {
classNameTranslations.put(fromName, toName);
return this;
}
public Builder addInterceptedMethod(MethodRef methodReference) {
interceptedMethods.add(methodReference);
return this;
}
public Builder addInstrumentedClass(String name) {
instrumentedClasses.add(name);
return this;
}
public Builder addInstrumentedPackage(String packageName) {
instrumentedPackages.add(packageName);
return this;
}
public Builder doNotInstrumentClass(String className) {
this.classesToNotInstrument.add(className);
return this;
}
public Builder withConfig(Config config) {
for (Class<?> clazz : config.shadows()) {
Implements annotation = clazz.getAnnotation(Implements.class);
if (annotation == null) {
throw new IllegalArgumentException(clazz + " is not annotated with @Implements");
}
String className = annotation.className();
if (className.isEmpty()) {
className = annotation.value().getName();
}
if (!className.isEmpty()) {
addInstrumentedClass(className);
}
}
for (String packageName : config.instrumentedPackages()) {
addInstrumentedPackage(packageName);
}
return this;
}
public InstrumentationConfiguration build() {
interceptedMethods.addAll(Intrinsics.allRefs());
classesToNotAcquire.addAll(stringify(
TestLifecycle.class,
ShadowWrangler.class,
AndroidManifest.class,
InstrumentingClassLoader.class,
SdkEnvironment.class,
SdkConfig.class,
RobolectricTestRunner.class,
RobolectricTestRunner.HelperTestRunner.class,
ResourcePath.class,
ResourceTable.class,
XmlBlock.class,
ClassHandler.class,
ClassHandler.Plan.class,
ShadowInvalidator.class,
RealObject.class,
Implements.class,
Implementation.class,
Instrument.class,
DoNotInstrument.class,
Config.class,
Transcript.class,
DirectObjectMarker.class,
DependencyJar.class,
ParallelUniverseInterface.class,
ShadowedObject.class,
TempDirectory.class
));
packagesToNotAcquire.addAll(Arrays.asList(
"java.",
"javax.",
"sun.",
"com.sun.",
"org.w3c.",
"org.xml.",
"org.junit",
"org.hamcrest",
"org.specs2", // allows for android projects with mixed scala\java tests to be
"scala.", // run with Maven Surefire (see the RoboSpecs project on github)
"kotlin.",
"com.almworks.sqlite4java" // Fix #958: SQLite native library must be loaded once.
));
classNameTranslations.put("java.net.ExtendedResponseCache", RoboExtendedResponseCache.class.getName());
classNameTranslations.put("java.net.ResponseSource", RoboResponseSource.class.getName());
classNameTranslations.put("java.nio.charset.Charsets", RoboCharsets.class.getName());
// Instrumenting these classes causes a wierd failure.
classesToNotInstrument.add("android.R");
classesToNotInstrument.add("android.R$styleable");
instrumentedPackages.addAll(Arrays.asList("dalvik.", "libcore.", "android.", "com.android.internal.", "org.apache.http.", "org.kxml2."));
for (ShadowProvider provider : ServiceLoader.load(ShadowProvider.class)) {
instrumentedPackages.addAll(Arrays.asList(provider.getProvidedPackageNames()));
}
return new InstrumentationConfiguration(classNameTranslations, interceptedMethods, instrumentedPackages, instrumentedClasses, classesToNotAcquire, packagesToNotAcquire, classesToNotInstrument);
}
}
public static Builder newBuilder() {
return new Builder();
}
private final List<String> instrumentedPackages;
private final Set<String> instrumentedClasses;
private final Set<String> classesToNotInstrument;
private final Map<String, String> classNameTranslations;
private final Set<MethodRef> interceptedMethods;
private final Set<String> classesToNotAcquire;
private final Set<String> packagesToNotAcquire;
private int cachedHashCode;
private InstrumentationConfiguration(Map<String, String> classNameTranslations, Collection<MethodRef> interceptedMethods, Collection<String> instrumentedPackages, Collection<String> instrumentedClasses, Collection<String> classesToNotAcquire, Collection<String> packagesToNotAquire, Collection<String> classesToNotInstrument) {
this.classNameTranslations = ImmutableMap.copyOf(classNameTranslations);
this.interceptedMethods = ImmutableSet.copyOf(interceptedMethods);
this.instrumentedPackages = ImmutableList.copyOf(instrumentedPackages);
this.instrumentedClasses = ImmutableSet.copyOf(instrumentedClasses);
this.classesToNotAcquire = ImmutableSet.copyOf(classesToNotAcquire);
this.packagesToNotAcquire = ImmutableSet.copyOf(packagesToNotAquire);
this.classesToNotInstrument = ImmutableSet.copyOf(classesToNotInstrument);
this.cachedHashCode = 0;
}
/**
* Determine if {@link org.robolectric.internal.bytecode.InstrumentingClassLoader} should instrument a given class.
*
* @param classInfo The class to check.
* @return True if the class should be instrumented.
*/
public boolean shouldInstrument(ClassInfo classInfo) {
return !(classInfo.isInterface()
|| classInfo.isAnnotation()
|| classInfo.hasAnnotation(DoNotInstrument.class))
&& (isInInstrumentedPackage(classInfo)
|| instrumentedClasses.contains(classInfo.getName())
|| classInfo.hasAnnotation(Instrument.class))
&& !(classesToNotInstrument.contains(classInfo.getName()));
}
/**
* Determine if {@link org.robolectric.internal.bytecode.InstrumentingClassLoader} should load a given class.
*
* @param name The fully-qualified class name.
* @return True if the class should be loaded.
*/
public boolean shouldAcquire(String name) {
// the org.robolectric.res and org.robolectric.manifest packages live in the base classloader, but not its tests; yuck.
int lastDot = name.lastIndexOf('.');
String pkgName = name.substring(0, lastDot == -1 ? 0 : lastDot);
if (pkgName.equals("org.robolectric.res") || (pkgName.equals("org.robolectric.manifest"))) {
return name.contains("Test");
}
// Internal android R class must be loaded from the framework resources in the framework jar.
if (name.matches("com\\.android\\.internal\\.R(\\$.*)?")) {
return true;
}
// Android SDK code almost universally refers to com.android.internal.R, except
// when refering to android.R.stylable, as in HorizontalScrollView. arghgh.
// See https://github.com/robolectric/robolectric/issues/521
if (name.startsWith("android.R")) {
return true;
}
// Hack. Fixes https://github.com/robolectric/robolectric/issues/1864
if (name.equals("javax.net.ssl.DistinguishedNameParser")) {
return true;
}
for (String packageName : packagesToNotAcquire) {
if (name.startsWith(packageName)) return false;
}
return !(name.matches(".*\\.R(|\\$[a-z]+)$") || classesToNotAcquire.contains(name));
}
public Set<MethodRef> methodsToIntercept() {
return Collections.unmodifiableSet(interceptedMethods);
}
/**
* Map from a requested class to an alternate stand-in, or not.
*
* @return Mapping of class name translations.
*/
public Map<String, String> classNameTranslations() {
return Collections.unmodifiableMap(classNameTranslations);
}
public boolean containsStubs(ClassInfo classInfo) {
return classInfo.getName().startsWith("com.google.android.maps.");
}
private static Collection<String> stringify(Class... classes) {
ArrayList<String> strings = new ArrayList<>();
for (Class aClass : classes) {
strings.add(aClass.getName());
}
return strings;
}
private boolean isInInstrumentedPackage(ClassInfo classInfo) {
final String className = classInfo.getName();
for (String instrumentedPackage : instrumentedPackages) {
if (className.startsWith(instrumentedPackage)) {
return true;
}
}
return false;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
InstrumentationConfiguration that = (InstrumentationConfiguration) o;
if (!classNameTranslations.equals(that.classNameTranslations)) return false;
if (!classesToNotAcquire.equals(that.classesToNotAcquire)) return false;
if (!instrumentedPackages.equals(that.instrumentedPackages)) return false;
if (!instrumentedClasses.equals(that.instrumentedClasses)) return false;
if (!interceptedMethods.equals(that.interceptedMethods)) return false;
return true;
}
@Override
public int hashCode() {
if (cachedHashCode != 0) {
return cachedHashCode;
}
int result = instrumentedPackages.hashCode();
result = 31 * result + instrumentedClasses.hashCode();
result = 31 * result + classNameTranslations.hashCode();
result = 31 * result + interceptedMethods.hashCode();
result = 31 * result + classesToNotAcquire.hashCode();
cachedHashCode = result;
return result;
}
}