All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.robolectric.internal.bytecode.InstrumentationConfiguration Maven / Gradle / Ivy

There is a newer version: 4.13
Show newest version
package org.robolectric.internal.bytecode;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.objectweb.asm.tree.MethodInsnNode;
import org.robolectric.annotation.internal.DoNotInstrument;
import org.robolectric.annotation.internal.Instrument;
import org.robolectric.shadow.api.Shadow;

/**
 * Configuration rules for {@link SandboxClassLoader}.
 */
public class InstrumentationConfiguration {

  public static Builder newBuilder() {
    return new Builder();
  }

  static final Set CLASSES_TO_ALWAYS_ACQUIRE = Sets.newHashSet(
      RobolectricInternals.class.getName(),
      InvokeDynamicSupport.class.getName(),
      Shadow.class.getName(),

      // these classes are deprecated and will be removed soon:
      "org.robolectric.util.FragmentTestUtil",
      "org.robolectric.util.FragmentTestUtil$FragmentUtilActivity"
  );

  // Must always acquire these as they change from API level to API level
  static final ImmutableSet RESOURCES_TO_ALWAYS_ACQUIRE =
      ImmutableSet.of("build.prop", "usr/share/zoneinfo/tzdata");

  private final List instrumentedPackages;
  private final Set instrumentedClasses;
  private final Set classesToNotInstrument;
  private final String classesToNotInstrumentRegex;
  private final Map classNameTranslations;
  private final Set interceptedMethods;
  private final Set classesToNotAcquire;
  private final Set packagesToNotAcquire;
  private final Set packagesToNotInstrument;
  private int cachedHashCode;

  private final TypeMapper typeMapper;
  private final Set methodsToIntercept;

  protected InstrumentationConfiguration(
      Map classNameTranslations,
      Collection interceptedMethods,
      Collection instrumentedPackages,
      Collection instrumentedClasses,
      Collection classesToNotAcquire,
      Collection packagesToNotAquire,
      Collection classesToNotInstrument,
      Collection packagesToNotInstrument,
      String classesToNotInstrumentRegex) {
    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.packagesToNotInstrument = ImmutableSet.copyOf(packagesToNotInstrument);
    this.classesToNotInstrumentRegex = classesToNotInstrumentRegex;
    this.cachedHashCode = 0;

    this.typeMapper = new TypeMapper(classNameTranslations());
    this.methodsToIntercept = ImmutableSet.copyOf(convertToSlashes(methodsToIntercept()));
  }

  /**
   * Determine if {@link SandboxClassLoader} should instrument a given class.
   *
   * @param classDetails The class to check.
   * @return True if the class should be instrumented.
   */
  public boolean shouldInstrument(ClassDetails classDetails) {
    return !classDetails.isAnnotation()
        && !classesToNotInstrument.contains(classDetails.getName())
        && !isInPackagesToNotInstrument(classDetails.getName())
        && !classMatchesExclusionRegex(classDetails.getName())
        && !classDetails.isInstrumented()
        && !classDetails.hasAnnotation(DoNotInstrument.class)
        && (isInInstrumentedPackage(classDetails.getName())
            || instrumentedClasses.contains(classDetails.getName())
            || classDetails.hasAnnotation(Instrument.class));
  }

  private boolean classMatchesExclusionRegex(String className) {
    return classesToNotInstrumentRegex != null && className.matches(classesToNotInstrumentRegex);
  }

  /**
   * Determine if {@link SandboxClassLoader} 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) {
    if (CLASSES_TO_ALWAYS_ACQUIRE.contains(name)) {
      return true;
    }

    if (name.equals("java.util.jar.StrictJarFile")) {
      return true;
    }

    // android.R and com.android.internal.R classes must be loaded from the framework jar
    if (name.matches("(android|com\\.android\\.internal)\\.R(\\$.+)?")) {
      return true;
    }

    // Hack. Fixes https://github.com/robolectric/robolectric/issues/1864
    if (name.equals("javax.net.ssl.DistinguishedNameParser")
        || name.equals("javax.microedition.khronos.opengles.GL")) {
      return true;
    }

    for (String packageName : packagesToNotAcquire) {
      if (name.startsWith(packageName)) return false;
    }

    // R classes must be loaded from system CP
    boolean isRClass = name.matches(".*\\.R(|\\$[a-z]+)$");
    return !isRClass && !classesToNotAcquire.contains(name);
  }

  /**
   * Determine if {@link SandboxClassLoader} should load a given resource.
   *
   * @param name The fully-qualified resource name.
   * @return True if the resource should be loaded.
   */
  public boolean shouldAcquireResource(String name) {
    return RESOURCES_TO_ALWAYS_ACQUIRE.contains(name);
  }

  public Set 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 classNameTranslations() {
    return Collections.unmodifiableMap(classNameTranslations);
  }

  private boolean isInInstrumentedPackage(String className) {
    for (String instrumentedPackage : instrumentedPackages) {
      if (className.startsWith(instrumentedPackage)) {
        return true;
      }
    }
    return false;
  }

  private boolean isInPackagesToNotInstrument(String className) {
    for (String notInstrumentedPackage : packagesToNotInstrument) {
      if (className.startsWith(notInstrumentedPackage)) {
        return true;
      }
    }
    return false;
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof InstrumentationConfiguration)) 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;
  }

  public String remapParamType(String desc) {
    return typeMapper.remapParamType(desc);
  }

  public String remapParams(String desc) {
    return typeMapper.remapParams(desc);
  }

  public String mappedTypeName(String internalName) {
    return typeMapper.mappedTypeName(internalName);
  }

  boolean shouldIntercept(MethodInsnNode targetMethod) {
    if (targetMethod.name.equals("")) {
      return false; // sorry, can't strip out calls to super() in constructor
    }
    return methodsToIntercept.contains(new MethodRef(targetMethod.owner, targetMethod.name))
        || methodsToIntercept.contains(new MethodRef(targetMethod.owner, "*"));
  }

  private static Set convertToSlashes(Set methodRefs) {
    HashSet transformed = new HashSet<>();
    for (MethodRef methodRef : methodRefs) {
      transformed.add(new MethodRef(internalize(methodRef.className), methodRef.methodName));
    }
    return transformed;
  }

  private static String internalize(String className) {
    return className.replace('.', '/');
  }

  public static final class Builder {
    public final Collection instrumentedPackages = new HashSet<>();
    public final Collection interceptedMethods = new HashSet<>();
    public final Map classNameTranslations = new HashMap<>();
    public final Collection classesToNotAcquire = new HashSet<>();
    public final Collection packagesToNotAcquire = new HashSet<>();
    public final Collection instrumentedClasses = new HashSet<>();
    public final Collection classesToNotInstrument = new HashSet<>();
    public final Collection packagesToNotInstrument = new HashSet<>();
    public String classesToNotInstrumentRegex;


    public Builder() {
    }

    public Builder(InstrumentationConfiguration classLoaderConfig) {
      instrumentedPackages.addAll(classLoaderConfig.instrumentedPackages);
      interceptedMethods.addAll(classLoaderConfig.interceptedMethods);
      classNameTranslations.putAll(classLoaderConfig.classNameTranslations);
      classesToNotAcquire.addAll(classLoaderConfig.classesToNotAcquire);
      packagesToNotAcquire.addAll(classLoaderConfig.packagesToNotAcquire);
      instrumentedClasses.addAll(classLoaderConfig.instrumentedClasses);
      classesToNotInstrument.addAll(classLoaderConfig.classesToNotInstrument);
      packagesToNotInstrument.addAll(classLoaderConfig.packagesToNotInstrument);
      classesToNotInstrumentRegex = classLoaderConfig.classesToNotInstrumentRegex;
    }

    public Builder doNotAcquireClass(Class clazz) {
      doNotAcquireClass(clazz.getName());
      return this;
    }

    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 doNotInstrumentPackage(String packageName) {
      this.packagesToNotInstrument.add(packageName);
      return this;
    }

    public Builder setDoNotInstrumentClassRegex(String classNameRegex) {
      this.classesToNotInstrumentRegex = classNameRegex;
      return this;
    }

    @SuppressWarnings("AndroidJdkLibsChecker")
    public InstrumentationConfiguration build() {
      // Remove redundant packages, e.g. remove 'android.os' if 'android.' is present.
      List minimalPackages = new ArrayList<>(instrumentedPackages);
      if (!instrumentedPackages.isEmpty()) {
        Collections.sort(minimalPackages);
        Iterator iterator = minimalPackages.iterator();
        String cur = iterator.next();
        while (iterator.hasNext()) {
          String element = iterator.next();
          if (element.startsWith(cur)) {
            iterator.remove();
          } else {
            cur = element;
          }
        }
      }
      // Remove redundant classes that are already specified by a package. We do this to avoid
      // unnecessarily creating sandboxes if a class is specified to be instrumented via
      // '@Config(shadows=...)'.
      List minimalClasses =
          instrumentedClasses.stream()
              .filter(className -> minimalPackages.stream().noneMatch(className::startsWith))
              .collect(Collectors.toList());

      return new InstrumentationConfiguration(
          classNameTranslations,
          interceptedMethods,
          minimalPackages,
          minimalClasses,
          classesToNotAcquire,
          packagesToNotAcquire,
          classesToNotInstrument,
          packagesToNotInstrument,
          classesToNotInstrumentRegex);
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy