diff options
author | Christian Williams <christianw@google.com> | 2017-01-25 13:52:23 -0800 |
---|---|---|
committer | Christian Williams <christianw@google.com> | 2017-02-07 15:07:55 -0800 |
commit | 58f07eef06aee72314f27ed2a24fa745243f83ef (patch) | |
tree | bd37fed4ca6b5bbaf2854193bf77a12d32a57914 /robolectric-sandbox | |
parent | 7980652753a7e4af50f935cad531a886be348f8a (diff) | |
download | robolectric-shadows-58f07eef06aee72314f27ed2a24fa745243f83ef.tar.gz |
Rename robolectric-instrumentation to robolectric-sandbox.
Diffstat (limited to 'robolectric-sandbox')
72 files changed, 5809 insertions, 0 deletions
diff --git a/robolectric-sandbox/build.gradle b/robolectric-sandbox/build.gradle new file mode 100644 index 000000000..870a31265 --- /dev/null +++ b/robolectric-sandbox/build.gradle @@ -0,0 +1,23 @@ +new RoboJavaModulePlugin( + deploy: true +).apply(project) + +dependencies { + // Compile dependencies + compile project(":robolectric-annotations") + compile project(":robolectric-utils") + compile project(":shadows-api") + + compile "org.ow2.asm:asm:5.0.1" +// compile "org.ow2.asm:asm-util:5.0.1" + compile "org.ow2.asm:asm-commons:5.0.1" +// compile "org.ow2.asm:asm-analysis:5.0.1" + compile "com.google.guava:guava:20.0" + compileOnly "com.intellij:annotations:12.0" + + testCompile "junit:junit:4.12" + testCompile "org.hamcrest:hamcrest-junit:2.0.0.0" + testCompile "org.assertj:assertj-core:2.6.0" + testCompile "org.mockito:mockito-core:2.5.4" + testCompile project(":robolectric-junit") +}
\ No newline at end of file diff --git a/robolectric-sandbox/src/main/java/org/robolectric/internal/DirectObjectMarker.java b/robolectric-sandbox/src/main/java/org/robolectric/internal/DirectObjectMarker.java new file mode 100644 index 000000000..3a98b00fb --- /dev/null +++ b/robolectric-sandbox/src/main/java/org/robolectric/internal/DirectObjectMarker.java @@ -0,0 +1,9 @@ +package org.robolectric.internal; + +public class DirectObjectMarker { + public static final DirectObjectMarker INSTANCE = new DirectObjectMarker() { + }; + + private DirectObjectMarker() { + } +} diff --git a/robolectric-sandbox/src/main/java/org/robolectric/internal/InvokeDynamic.java b/robolectric-sandbox/src/main/java/org/robolectric/internal/InvokeDynamic.java new file mode 100644 index 000000000..94a37a510 --- /dev/null +++ b/robolectric-sandbox/src/main/java/org/robolectric/internal/InvokeDynamic.java @@ -0,0 +1,23 @@ +package org.robolectric.internal; + +import org.robolectric.util.JavaVersion; + +public class InvokeDynamic { + public static final boolean ENABLED = useInvokeDynamic(); + + private static final String ENABLE_INVOKEDYNAMIC = "robolectric.invokedynamic.enable"; + // We currently crash on versions earlier than 8u40 because of a bug in the C2 compiler. + // This seems to be the bug http://bugs.java.com/view_bug.do?bug_id=8059556 but I have been + // unable to pinpoint exactly why this affects us. + private static final String INVOKEDYNAMIC_MINIMUM_VERSION = "1.8.0_40"; + + private static boolean useInvokeDynamic() { + String property = System.getProperty(ENABLE_INVOKEDYNAMIC); + if (property != null) { + return Boolean.valueOf(property); + } else { + JavaVersion javaVersion = new JavaVersion(System.getProperty("java.version")); + return javaVersion.compareTo(new JavaVersion(INVOKEDYNAMIC_MINIMUM_VERSION)) >= 0; + } + } +} diff --git a/robolectric-sandbox/src/main/java/org/robolectric/internal/ProxyMaker.java b/robolectric-sandbox/src/main/java/org/robolectric/internal/ProxyMaker.java new file mode 100644 index 000000000..f44c862cc --- /dev/null +++ b/robolectric-sandbox/src/main/java/org/robolectric/internal/ProxyMaker.java @@ -0,0 +1,117 @@ +package org.robolectric.internal; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.GeneratorAdapter; +import org.objectweb.asm.commons.Method; +import sun.misc.Unsafe; + +import static org.objectweb.asm.Opcodes.ACC_FINAL; +import static org.objectweb.asm.Opcodes.ACC_PUBLIC; +import static org.objectweb.asm.Opcodes.ACC_SUPER; +import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL; +import static org.objectweb.asm.Opcodes.V1_7; + +public class ProxyMaker { + private static final String TARGET_FIELD = "__proxy__"; + private static final String PROXY_NAME = + Type.getInternalName(ProxyMaker.class) + "$GeneratedProxy"; + private static final Type PROXY_TYPE = Type.getType(PROXY_NAME); + + private static final Unsafe UNSAFE; + private static final MethodHandles.Lookup LOOKUP = MethodHandles.publicLookup(); + + static { + try { + Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe"); + unsafeField.setAccessible(true); + UNSAFE = (Unsafe) unsafeField.get(null); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new AssertionError(e); + } + } + + private final MethodMapper methodMapper; + private final ClassValue<Factory> factories; + + public ProxyMaker(MethodMapper methodMapper) { + this.methodMapper = methodMapper; + factories = new ClassValue<Factory>() { + @Override protected Factory computeValue(Class<?> type) { + return createProxyFactory(type); + } + }; + } + + public <T> T createProxy(Class<T> targetClass, T target) { + return factories.get(targetClass).createProxy(targetClass, target); + } + + <T> Factory createProxyFactory(Class<T> targetClass) { + Type targetType = Type.getType(targetClass); + String targetName = targetType.getInternalName(); + ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES| ClassWriter.COMPUTE_MAXS); + writer.visit(V1_7, ACC_PUBLIC | ACC_SUPER | ACC_FINAL, PROXY_NAME, null, targetName, null); + + writer.visitField(ACC_PUBLIC, TARGET_FIELD, targetType.getDescriptor(), null, null); + + for (java.lang.reflect.Method method : targetClass.getMethods()) { + if (!shouldProxy(method)) continue; + + Method proxyMethod = Method.getMethod(method); + GeneratorAdapter m = new GeneratorAdapter(ACC_PUBLIC, Method.getMethod(method), null, null, writer); + m.loadThis(); + m.getField(PROXY_TYPE, TARGET_FIELD, targetType); + m.loadArgs(); + String targetMethod = methodMapper.getName(targetClass.getName(), method.getName()); + // In Java 8 we could use invokespecial here but not in 7, from jvm spec: + // If an invokespecial instruction names a method which is not an instance + // initialization method, then the type of the target reference on the operand + // stack must be assignment compatible with the current class (JLS §5.2). + m.visitMethodInsn(INVOKEVIRTUAL, targetName, targetMethod, proxyMethod.getDescriptor(), false); + m.returnValue(); + m.endMethod(); + } + + writer.visitEnd(); + + final Class<?> proxyClass = UNSAFE.defineAnonymousClass(targetClass, writer.toByteArray(), null); + + try { + final MethodHandle setter = LOOKUP.findSetter(proxyClass, TARGET_FIELD, targetClass); + return new Factory() { + @Override public <E> E createProxy(Class<E> targetClass, E target) { + try { + Object proxy = UNSAFE.allocateInstance(proxyClass); + + setter.invoke(proxy, target); + + return targetClass.cast(proxy); + } catch (Throwable t) { + throw new AssertionError(t); + } + } + }; + } catch (IllegalAccessException | NoSuchFieldException e) { + throw new AssertionError(e); + } + } + + private static boolean shouldProxy(java.lang.reflect.Method method) { + int modifiers = method.getModifiers(); + return !Modifier.isAbstract(modifiers) && !Modifier.isFinal(modifiers) && !Modifier.isPrivate( + modifiers) && !Modifier.isNative(modifiers); + } + + interface MethodMapper { + String getName(String className, String methodName); + } + + interface Factory { + <T> T createProxy(Class<T> targetClass, T target); + } +} diff --git a/robolectric-sandbox/src/main/java/org/robolectric/internal/ShadowConstants.java b/robolectric-sandbox/src/main/java/org/robolectric/internal/ShadowConstants.java new file mode 100644 index 000000000..3cf5f2d8e --- /dev/null +++ b/robolectric-sandbox/src/main/java/org/robolectric/internal/ShadowConstants.java @@ -0,0 +1,9 @@ +package org.robolectric.internal; + +public class ShadowConstants { + public static final String ROBO_PREFIX = "$$robo$$"; + public static final String CLASS_HANDLER_DATA_FIELD_NAME = "__robo_data__"; // todo: rename + public static final String STATIC_INITIALIZER_METHOD_NAME = "__staticInitializer__"; + public static final String CONSTRUCTOR_METHOD_NAME = "__constructor__"; + public static final String GET_ROBO_DATA_METHOD_NAME = "$$robo$getData"; +} diff --git a/robolectric-sandbox/src/main/java/org/robolectric/internal/ShadowImpl.java b/robolectric-sandbox/src/main/java/org/robolectric/internal/ShadowImpl.java new file mode 100644 index 000000000..ae99a00da --- /dev/null +++ b/robolectric-sandbox/src/main/java/org/robolectric/internal/ShadowImpl.java @@ -0,0 +1,68 @@ +package org.robolectric.internal; + +import org.robolectric.util.ReflectionHelpers; + +public class ShadowImpl implements IShadow { + + private final ProxyMaker PROXY_MAKER = new ProxyMaker(new ProxyMaker.MethodMapper() { + @Override public String getName(String className, String methodName) { + return directMethodName(methodName); + } + }); + + public <T> T newInstanceOf(Class<T> clazz) { + return ReflectionHelpers.callConstructor(clazz); + } + + public <T> T newInstance(Class<T> clazz, Class[] parameterTypes, Object[] params) { + return ReflectionHelpers.callConstructor(clazz, ReflectionHelpers.ClassParameter.fromComponentLists(parameterTypes, params)); + } + + public <T> T directlyOn(T shadowedObject, Class<T> clazz) { + return createProxy(shadowedObject, clazz); + } + + private <T> T createProxy(T shadowedObject, Class<T> clazz) { + try { + if (InvokeDynamic.ENABLED) { + return PROXY_MAKER.createProxy(clazz, shadowedObject); + } else { + return ReflectionHelpers.callConstructor(clazz, + ReflectionHelpers.ClassParameter.fromComponentLists(new Class[] { DirectObjectMarker.class, clazz }, + new Object[] { DirectObjectMarker.INSTANCE, shadowedObject })); + } + } catch (Exception e) { + throw new RuntimeException("error creating direct call proxy for " + clazz, e); + } + } + + @SuppressWarnings("unchecked") + public <R> R directlyOn(Object shadowedObject, String clazzName, String methodName, ReflectionHelpers.ClassParameter... paramValues) { + try { + Class<Object> aClass = (Class<Object>) shadowedObject.getClass().getClassLoader().loadClass(clazzName); + return directlyOn(shadowedObject, aClass, methodName, paramValues); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + + public <R, T> R directlyOn(T shadowedObject, Class<T> clazz, String methodName, ReflectionHelpers.ClassParameter... paramValues) { + String directMethodName = directMethodName(methodName); + return (R) ReflectionHelpers.callInstanceMethod(clazz, shadowedObject, directMethodName, paramValues); + } + + public <R, T> R directlyOn(Class<T> clazz, String methodName, ReflectionHelpers.ClassParameter... paramValues) { + String directMethodName = directMethodName(methodName); + return (R) ReflectionHelpers.callStaticMethod(clazz, directMethodName, paramValues); + } + + public <R> R invokeConstructor(Class<? extends R> clazz, R instance, ReflectionHelpers.ClassParameter... paramValues) { + String directMethodName = directMethodName(ShadowConstants.CONSTRUCTOR_METHOD_NAME); + return (R) ReflectionHelpers.callInstanceMethod(clazz, instance, directMethodName, paramValues); + } + + public String directMethodName(String methodName) { + return ShadowConstants.ROBO_PREFIX + methodName; + } + +} diff --git a/robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/ClassHandler.java b/robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/ClassHandler.java new file mode 100644 index 000000000..0dde43c8c --- /dev/null +++ b/robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/ClassHandler.java @@ -0,0 +1,28 @@ +package org.robolectric.internal.bytecode; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodType; + +public interface ClassHandler { + void classInitializing(Class clazz); + + Object initializing(Object instance); + + Plan methodInvoked(String signature, boolean isStatic, Class<?> theClass); + + MethodHandle getShadowCreator(Class<?> caller); + + MethodHandle findShadowMethod(Class<?> theClass, String name, MethodType type, + boolean isStatic) + throws IllegalAccessException; + + Object intercept(String signature, Object instance, Object[] params, Class theClass) throws Throwable; + + <T extends Throwable> T stripStackTrace(T throwable); + + public interface Plan { + Object run(Object instance, Object roboData, Object[] params) throws Throwable; + + String describe(); + } +} diff --git a/robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/ClassInfo.java b/robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/ClassInfo.java new file mode 100644 index 000000000..5f021cb6f --- /dev/null +++ b/robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/ClassInfo.java @@ -0,0 +1,38 @@ +package org.robolectric.internal.bytecode; + +import java.lang.annotation.Annotation; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.AnnotationNode; +import org.objectweb.asm.tree.ClassNode; + +public class ClassInfo { + private final String className; + private final ClassNode classNode; + + public ClassInfo(String className, ClassNode classNode) { + this.className = className; + this.classNode = classNode; + } + + public boolean isInterface() { + return (classNode.access & Opcodes.ACC_INTERFACE) != 0; + } + + public boolean isAnnotation() { + return (classNode.access & Opcodes.ACC_ANNOTATION) != 0; + } + + public boolean hasAnnotation(Class<? extends Annotation> annotationClass) { + String internalName = "L" + annotationClass.getName().replace('.', '/') + ";"; + if (classNode.visibleAnnotations == null) return false; + for (Object visibleAnnotation : classNode.visibleAnnotations) { + AnnotationNode annotationNode = (AnnotationNode) visibleAnnotation; + if (annotationNode.desc.equals(internalName)) return true; + } + return false; + } + + public String getName() { + return className; + } +} diff --git a/robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/InstrumentationConfiguration.java b/robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/InstrumentationConfiguration.java new file mode 100644 index 000000000..a9a610106 --- /dev/null +++ b/robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/InstrumentationConfiguration.java @@ -0,0 +1,233 @@ +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 org.robolectric.annotation.internal.DoNotInstrument; +import org.robolectric.annotation.internal.Instrument; +import org.robolectric.internal.Shadow; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Configuration rules for {@link org.robolectric.internal.bytecode.InstrumentingClassLoader}. + */ +public class InstrumentationConfiguration { + public static Builder newBuilder() { + return new Builder(); + } + + private static final Set<String> CLASSES_TO_ALWAYS_ACQUIRE = Sets.newHashSet( + RobolectricInternals.class.getName(), + InvokeDynamicSupport.class.getName(), + Shadow.class.getName() + ); + + 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) { + if (CLASSES_TO_ALWAYS_ACQUIRE.contains(name)) { + return true; + } + + // 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; + } + + boolean isRClass = name.matches(".*\\.R(|\\$[a-z]+)$"); + return !isRClass && !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 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; + } + + 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() { + } + + 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); + } + + 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 InstrumentationConfiguration build() { + return new InstrumentationConfiguration( + classNameTranslations, interceptedMethods, instrumentedPackages, + instrumentedClasses, classesToNotAcquire, packagesToNotAcquire, classesToNotInstrument); + } + } +} diff --git a/robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/InstrumentingClassLoader.java b/robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/InstrumentingClassLoader.java new file mode 100644 index 000000000..d16ffc022 --- /dev/null +++ b/robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/InstrumentingClassLoader.java @@ -0,0 +1,1332 @@ +package org.robolectric.internal.bytecode; + +import org.jetbrains.annotations.NotNull; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.Handle; +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.GeneratorAdapter; +import org.objectweb.asm.commons.JSRInlinerAdapter; +import org.objectweb.asm.commons.Method; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.FieldInsnNode; +import org.objectweb.asm.tree.FieldNode; +import org.objectweb.asm.tree.InsnList; +import org.objectweb.asm.tree.InsnNode; +import org.objectweb.asm.tree.InvokeDynamicInsnNode; +import org.objectweb.asm.tree.LdcInsnNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.TypeInsnNode; +import org.objectweb.asm.tree.VarInsnNode; +import org.robolectric.internal.DirectObjectMarker; +import org.robolectric.internal.InvokeDynamic; +import org.robolectric.internal.ShadowConstants; +import org.robolectric.internal.ShadowImpl; +import org.robolectric.internal.ShadowedObject; +import org.robolectric.util.Logger; +import org.robolectric.util.Util; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.invoke.CallSite; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.Modifier; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.Set; + +import static java.lang.invoke.MethodType.methodType; +import static org.objectweb.asm.Type.ARRAY; +import static org.objectweb.asm.Type.OBJECT; +import static org.objectweb.asm.Type.VOID; + +/** + * Class loader that modifies the bytecode of Android classes to insert calls to Robolectric's shadow classes. + */ +public class InstrumentingClassLoader extends ClassLoader implements Opcodes { + + private final URLClassLoader urls; + private final InstrumentationConfiguration config; + private final Map<String, Class> classes = new HashMap<>(); + private final Map<String, String> classesToRemap; + private final Set<MethodRef> methodsToIntercept; + + public InstrumentingClassLoader(InstrumentationConfiguration config, URL... urls) { + super(InstrumentingClassLoader.class.getClassLoader()); + this.config = config; + this.urls = new URLClassLoader(urls, null); + classesToRemap = convertToSlashes(config.classNameTranslations()); + methodsToIntercept = convertToSlashes(config.methodsToIntercept()); + for (URL url : urls) { + Logger.debug("Loading classes from: %s", url); + } + } + + @Override + synchronized public Class loadClass(String name) throws ClassNotFoundException { + Class<?> theClass = classes.get(name); + if (theClass != null) { + if (theClass == MissingClassMarker.class) { + throw new ClassNotFoundException(name); + } else { + return theClass; + } + } + + try { + if (config.shouldAcquire(name)) { + theClass = findClass(name); + } else { + theClass = getParent().loadClass(name); + } + } catch (ClassNotFoundException e) { + classes.put(name, MissingClassMarker.class); + throw e; + } + + classes.put(name, theClass); + return theClass; + } + + @Override + public URL getResource(String name) { + URL fromParent = super.getResource(name); + if (fromParent != null) { + return fromParent; + } + return urls.getResource(name); + } + + private InputStream getClassBytesAsStreamPreferringLocalUrls(String resName) { + InputStream fromUrlsClassLoader = urls.getResourceAsStream(resName); + if (fromUrlsClassLoader != null) { + return fromUrlsClassLoader; + } + return super.getResourceAsStream(resName); + } + + @Override + protected Class<?> findClass(final String className) throws ClassNotFoundException { + if (config.shouldAcquire(className)) { + final byte[] origClassBytes = getByteCode(className); + + ClassNode classNode = new ClassNode(Opcodes.ASM4) { + @Override + public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) { + desc = remapParamType(desc); + return super.visitField(access, name, desc, signature, value); + } + + @Override + public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { + MethodVisitor methodVisitor = super.visitMethod(access, name, remapParams(desc), signature, exceptions); + return new JSRInlinerAdapter(methodVisitor, access, name, desc, signature, exceptions); + } + }; + + final ClassReader classReader = new ClassReader(origClassBytes); + classReader.accept(classNode, 0); + + classNode.interfaces.add(Type.getInternalName(ShadowedObject.class)); + + try { + byte[] bytes; + ClassInfo classInfo = new ClassInfo(className, classNode); + if (config.shouldInstrument(classInfo)) { + bytes = getInstrumentedBytes(classNode, config.containsStubs(classInfo)); + } else { + bytes = origClassBytes; + } + ensurePackage(className); + return defineClass(className, bytes, 0, bytes.length); + } catch (Exception e) { + throw new ClassNotFoundException("couldn't load " + className, e); + } catch (OutOfMemoryError e) { + System.err.println("[ERROR] couldn't load " + className + " in " + this); + throw e; + } + } else { + throw new IllegalStateException("how did we get here? " + className); + } + } + + protected byte[] getByteCode(String className) throws ClassNotFoundException { + String classFilename = className.replace('.', '/') + ".class"; + try (InputStream classBytesStream = getClassBytesAsStreamPreferringLocalUrls(classFilename)) { + if (classBytesStream == null) throw new ClassNotFoundException(className); + + return Util.readBytes(classBytesStream); + } catch (IOException e) { + throw new ClassNotFoundException("couldn't load " + className, e); + } + } + + private void ensurePackage(final String className) { + int lastDotIndex = className.lastIndexOf('.'); + if (lastDotIndex != -1) { + String pckgName = className.substring(0, lastDotIndex); + Package pckg = getPackage(pckgName); + if (pckg == null) { + definePackage(pckgName, null, null, null, null, null, null, null); + } + } + } + + private String remapParams(String desc) { + StringBuilder buf = new StringBuilder(); + buf.append("("); + for (Type type : Type.getArgumentTypes(desc)) { + buf.append(remapParamType(type)); + } + buf.append(")"); + buf.append(remapParamType(Type.getReturnType(desc))); + return buf.toString(); + } + + // remap Landroid/Foo; to Landroid/Bar; + private String remapParamType(String desc) { + return remapParamType(Type.getType(desc)); + } + + private String remapParamType(Type type) { + String remappedName; + String internalName; + + switch (type.getSort()) { + case ARRAY: + internalName = type.getInternalName(); + int count = 0; + while (internalName.charAt(count) == '[') count++; + + remappedName = remapParamType(internalName.substring(count)); + if (remappedName != null) { + return Type.getObjectType(internalName.substring(0, count) + remappedName).getDescriptor(); + } + break; + + case OBJECT: + internalName = type.getInternalName(); + remappedName = classesToRemap.get(internalName); + if (remappedName != null) { + return Type.getObjectType(remappedName).getDescriptor(); + } + break; + + default: + break; + } + return type.getDescriptor(); + } + + // remap android/Foo to android/Bar + private String remapType(String value) { + String remappedValue = classesToRemap.get(value); + if (remappedValue != null) { + value = remappedValue; + } + return value; + } + + private byte[] getInstrumentedBytes(ClassNode classNode, boolean containsStubs) throws ClassNotFoundException { + if (InvokeDynamic.ENABLED) { + new InvokeDynamicClassInstrumentor(classNode, containsStubs).instrument(); + } else { + new OldClassInstrumentor(classNode, containsStubs).instrument(); + } + ClassWriter writer = new InstrumentingClassWriter(classNode); + classNode.accept(writer); + return writer.toByteArray(); + } + + private Map<String, String> convertToSlashes(Map<String, String> map) { + HashMap<String, String> newMap = new HashMap<>(); + for (Map.Entry<String, String> entry : map.entrySet()) { + String key = internalize(entry.getKey()); + String value = internalize(entry.getValue()); + newMap.put(key, value); + newMap.put("L" + key + ";", "L" + value + ";"); // also the param reference form + } + return newMap; + } + + private Set<MethodRef> convertToSlashes(Set<MethodRef> methodRefs) { + HashSet<MethodRef> transformed = new HashSet<>(); + for (MethodRef methodRef : methodRefs) { + transformed.add(new MethodRef(internalize(methodRef.className), methodRef.methodName)); + } + return transformed; + } + + private String internalize(String className) { + return className.replace('.', '/'); + } + + public static void box(final Type type, ListIterator<AbstractInsnNode> instructions) { + if (type.getSort() == Type.OBJECT || type.getSort() == Type.ARRAY) { + return; + } + + if (type == Type.VOID_TYPE) { + instructions.add(new InsnNode(ACONST_NULL)); + } else { + Type boxed = getBoxedType(type); + instructions.add(new TypeInsnNode(NEW, boxed.getInternalName())); + if (type.getSize() == 2) { + // Pp -> Ppo -> oPpo -> ooPpo -> ooPp -> o + instructions.add(new InsnNode(DUP_X2)); + instructions.add(new InsnNode(DUP_X2)); + instructions.add(new InsnNode(POP)); + } else { + // p -> po -> opo -> oop -> o + instructions.add(new InsnNode(DUP_X1)); + instructions.add(new InsnNode(SWAP)); + } + instructions.add(new MethodInsnNode(INVOKESPECIAL, boxed.getInternalName(), "<init>", "(" + type.getDescriptor() + ")V")); + } + } + + private static Type getBoxedType(final Type type) { + switch (type.getSort()) { + case Type.BYTE: + return Type.getObjectType("java/lang/Byte"); + case Type.BOOLEAN: + return Type.getObjectType("java/lang/Boolean"); + case Type.SHORT: + return Type.getObjectType("java/lang/Short"); + case Type.CHAR: + return Type.getObjectType("java/lang/Character"); + case Type.INT: + return Type.getObjectType("java/lang/Integer"); + case Type.FLOAT: + return Type.getObjectType("java/lang/Float"); + case Type.LONG: + return Type.getObjectType("java/lang/Long"); + case Type.DOUBLE: + return Type.getObjectType("java/lang/Double"); + } + return type; + } + + private boolean shouldIntercept(MethodInsnNode targetMethod) { + if (targetMethod.name.equals("<init>")) 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, "*")); + } + + abstract class ClassInstrumentor { + private static final String ROBO_INIT_METHOD_NAME = "$$robo$init"; + static final String GET_ROBO_DATA_SIGNATURE = "()Ljava/lang/Object;"; + final Type OBJECT_TYPE = Type.getType(Object.class); + private final String OBJECT_DESC = Type.getDescriptor(Object.class); + + final ClassNode classNode; + private final boolean containsStubs; + final String internalClassName; + private final String className; + final Type classType; + + public ClassInstrumentor(ClassNode classNode, boolean containsStubs) { + this.classNode = classNode; + this.containsStubs = containsStubs; + + this.internalClassName = classNode.name; + this.className = classNode.name.replace('/', '.'); + this.classType = Type.getObjectType(internalClassName); + } + + //todo javadoc. Extract blocks to separate methods. + public void instrument() { + makeClassPublic(classNode); + classNode.access = classNode.access & ~ACC_FINAL; + + // Need Java version >=7 to allow invokedynamic + classNode.version = Math.max(classNode.version, V1_7); + + classNode.fields.add(0, new FieldNode(ACC_PUBLIC | ACC_FINAL, + ShadowConstants.CLASS_HANDLER_DATA_FIELD_NAME, OBJECT_DESC, OBJECT_DESC, null)); + + Set<String> foundMethods = instrumentMethods(); + + // If there is no constructor, adds one + addNoArgsConstructor(foundMethods); + + addDirectCallConstructor(); + + // Do not override final #equals, #hashCode, and #toString for all classes + instrumentInheritedObjectMethod(classNode, foundMethods, "equals", "(Ljava/lang/Object;)Z"); + instrumentInheritedObjectMethod(classNode, foundMethods, "hashCode", "()I"); + instrumentInheritedObjectMethod(classNode, foundMethods, "toString", "()Ljava/lang/String;"); + + addRoboInitMethod(); + + addRoboGetDataMethod(); + + doSpecialHandling(); + } + + @NotNull + private Set<String> instrumentMethods() { + Set<String> foundMethods = new HashSet<>(); + List<MethodNode> methods = new ArrayList<>(classNode.methods); + for (MethodNode method : methods) { + foundMethods.add(method.name + method.desc); + + filterSpecialMethods(method); + + if (method.name.equals("<clinit>")) { + method.name = ShadowConstants.STATIC_INITIALIZER_METHOD_NAME; + classNode.methods.add(generateStaticInitializerNotifierMethod()); + } else if (method.name.equals("<init>")) { + instrumentConstructor(method); + } else if (!isSyntheticAccessorMethod(method) && !Modifier.isAbstract(method.access)) { + instrumentNormalMethod(method); + } + } + return foundMethods; + } + + private void addNoArgsConstructor(Set<String> foundMethods) { + if (!foundMethods.contains("<init>()V")) { + MethodNode defaultConstructor = new MethodNode(ACC_PUBLIC, "<init>", "()V", "()V", null); + RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(defaultConstructor); + generator.loadThis(); + generator.visitMethodInsn(INVOKESPECIAL, classNode.superName, "<init>", "()V"); + generator.loadThis(); + generator.invokeVirtual(classType, new Method(ROBO_INIT_METHOD_NAME, "()V")); + generator.returnValue(); + classNode.methods.add(defaultConstructor); + } + } + + abstract protected void addDirectCallConstructor(); + + private void addRoboInitMethod() { + MethodNode initMethodNode = new MethodNode(ACC_PROTECTED, ROBO_INIT_METHOD_NAME, "()V", null, null); + RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(initMethodNode); + Label alreadyInitialized = new Label(); + generator.loadThis(); // this + generator.getField(classType, ShadowConstants.CLASS_HANDLER_DATA_FIELD_NAME, OBJECT_TYPE); // contents of __robo_data__ + generator.ifNonNull(alreadyInitialized); + generator.loadThis(); // this + generator.loadThis(); // this, this + writeCallToInitializing(generator); + // this, __robo_data__ + generator.putField(classType, ShadowConstants.CLASS_HANDLER_DATA_FIELD_NAME, OBJECT_TYPE); + generator.mark(alreadyInitialized); + generator.returnValue(); + classNode.methods.add(initMethodNode); + } + + abstract protected void writeCallToInitializing(RobolectricGeneratorAdapter generator); + + private void addRoboGetDataMethod() { + MethodNode initMethodNode = new MethodNode(ACC_PUBLIC, ShadowConstants.GET_ROBO_DATA_METHOD_NAME, GET_ROBO_DATA_SIGNATURE, null, null); + RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(initMethodNode); + generator.loadThis(); // this + generator.getField(classType, ShadowConstants.CLASS_HANDLER_DATA_FIELD_NAME, OBJECT_TYPE); // contents of __robo_data__ + generator.returnValue(); + generator.endMethod(); + classNode.methods.add(initMethodNode); + } + + private void doSpecialHandling() { + if (className.equals("android.os.Build$VERSION")) { + for (Object field : classNode.fields) { + FieldNode fieldNode = (FieldNode) field; + fieldNode.access &= ~(Modifier.FINAL); + } + } + } + + /** + * Checks if the given method in the class if overriding, at some point of it's + * inheritance tree, a final method + */ + private boolean isOverridingFinalMethod(ClassNode classNode, String methodName, String methodSignature) { + while(true) { + List<MethodNode> methods = new ArrayList<>(classNode.methods); + + for (MethodNode method : methods) { + if(method.name.equals(methodName) && method.desc.equals(methodSignature)) { + if ((method.access & ACC_FINAL) != 0) { + return true; + } + } + } + + if(classNode.superName == null) { + return false; + } + + try { + byte[] byteCode = getByteCode(classNode.superName); + ClassReader classReader = new ClassReader(byteCode); + classNode = new ClassNode(); + classReader.accept(classNode, 0); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } + + } + } + + private boolean isSyntheticAccessorMethod(MethodNode method) { + return (method.access & ACC_SYNTHETIC) != 0; + } + + /** + * To be used to instrument methods inherited from the Object class, + * such as hashCode, equals, and toString. + * Adds the methods directly to the class. + */ + private void instrumentInheritedObjectMethod(ClassNode classNode, Set<String> foundMethods, final String methodName, String methodDesc) { + // Won't instrument if method is overriding a final method + if (isOverridingFinalMethod(classNode, methodName, methodDesc)) { + return; + } + + // if the class doesn't directly override the method, it adds it as a direct invocation and instruments it + if (!foundMethods.contains(methodName + methodDesc)) { + MethodNode methodNode = new MethodNode(ACC_PUBLIC, methodName, methodDesc, null, null); + RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(methodNode); + generator.invokeMethod("java/lang/Object", methodNode); + generator.returnValue(); + generator.endMethod(); + this.classNode.methods.add(methodNode); + instrumentNormalMethod(methodNode); + } + } + + private void instrumentConstructor(MethodNode method) { + makeMethodPrivate(method); + + if (containsStubs) { + method.instructions.clear(); + + RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(method); + generator.loadThis(); + generator.visitMethodInsn(INVOKESPECIAL, classNode.superName, "<init>", "()V"); + generator.returnValue(); + generator.endMethod(); + } + + InsnList removedInstructions = extractCallToSuperConstructor(method); + method.name = new ShadowImpl().directMethodName(ShadowConstants.CONSTRUCTOR_METHOD_NAME); + classNode.methods.add(redirectorMethod(method, ShadowConstants.CONSTRUCTOR_METHOD_NAME)); + + String[] exceptions = exceptionArray(method); + MethodNode methodNode = new MethodNode(method.access, "<init>", method.desc, method.signature, exceptions); + makeMethodPublic(methodNode); + RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(methodNode); + + methodNode.instructions = removedInstructions; + + generator.loadThis(); + generator.invokeVirtual(classType, new Method(ROBO_INIT_METHOD_NAME, "()V")); + generateShadowCall(method, ShadowConstants.CONSTRUCTOR_METHOD_NAME, generator); + + generator.endMethod(); + classNode.methods.add(methodNode); + } + + private InsnList extractCallToSuperConstructor(MethodNode ctor) { + InsnList removedInstructions = new InsnList(); + int startIndex = 0; + + AbstractInsnNode[] insns = ctor.instructions.toArray(); + for (int i = 0; i < insns.length; i++) { + AbstractInsnNode node = insns[i]; + + switch (node.getOpcode()) { + case ALOAD: + VarInsnNode vnode = (VarInsnNode) node; + if (vnode.var == 0) { + startIndex = i; + } + break; + + case INVOKESPECIAL: + MethodInsnNode mnode = (MethodInsnNode) node; + if (mnode.owner.equals(internalClassName) || mnode.owner.equals(classNode.superName)) { + assert mnode.name.equals("<init>"); + + // remove all instructions in the range startIndex..i, from aload_0 to invokespecial <init> + while (startIndex <= i) { + ctor.instructions.remove(insns[startIndex]); + removedInstructions.add(insns[startIndex]); + startIndex++; + } + return removedInstructions; + } + break; + + case ATHROW: + ctor.visitCode(); + ctor.visitInsn(RETURN); + ctor.visitEnd(); + return removedInstructions; + } + } + + throw new RuntimeException("huh? " + ctor.name + ctor.desc); + } + + //TODO javadocs + private void instrumentNormalMethod(MethodNode method) { + // if not abstract, set a final modifier + if ((method.access & ACC_ABSTRACT) == 0) { + method.access = method.access | ACC_FINAL; + } + // if a native method, remove native modifier and force return a default value + if ((method.access & ACC_NATIVE) != 0) { + method.access = method.access & ~ACC_NATIVE; + + RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(method); + Type returnType = generator.getReturnType(); + generator.pushDefaultReturnValueToStack(returnType); + generator.returnValue(); + } + + // todo figure out + String originalName = method.name; + method.name = new ShadowImpl().directMethodName(originalName); + + MethodNode delegatorMethodNode = new MethodNode(method.access, originalName, method.desc, method.signature, exceptionArray(method)); + delegatorMethodNode.visibleAnnotations = method.visibleAnnotations; + delegatorMethodNode.access &= ~(ACC_NATIVE | ACC_ABSTRACT | ACC_FINAL); + + makeMethodPrivate(method); + + RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(delegatorMethodNode); + + generateShadowCall(method, originalName, generator); + + generator.endMethod(); + + classNode.methods.add(delegatorMethodNode); + } + + //todo rename + private MethodNode redirectorMethod(MethodNode method, String newName) { + MethodNode redirector = new MethodNode(ASM4, newName, method.desc, method.signature, exceptionArray(method)); + redirector.access = method.access & ~(ACC_NATIVE | ACC_ABSTRACT | ACC_FINAL); + makeMethodPrivate(redirector); + RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(redirector); + generator.invokeMethod(internalClassName, method); + generator.returnValue(); + return redirector; + } + + private String[] exceptionArray(MethodNode method) { + return ((List<String>) method.exceptions).toArray(new String[method.exceptions.size()]); + } + + /** + * Filters methods that might need special treatment because of various reasons + */ + private void filterSpecialMethods(MethodNode callingMethod) { + ListIterator<AbstractInsnNode> instructions = callingMethod.instructions.iterator(); + while (instructions.hasNext()) { + AbstractInsnNode node = instructions.next(); + + switch (node.getOpcode()) { + case NEW: + TypeInsnNode newInsnNode = (TypeInsnNode) node; + newInsnNode.desc = remapType(newInsnNode.desc); + break; + + case GETFIELD: + /* falls through */ + case PUTFIELD: + /* falls through */ + case GETSTATIC: + /* falls through */ + case PUTSTATIC: + FieldInsnNode fieldInsnNode = (FieldInsnNode) node; + fieldInsnNode.desc = remapType(fieldInsnNode.desc); // todo test + break; + + case INVOKESTATIC: + /* falls through */ + case INVOKEINTERFACE: + /* falls through */ + case INVOKESPECIAL: + /* falls through */ + case INVOKEVIRTUAL: + MethodInsnNode targetMethod = (MethodInsnNode) node; + targetMethod.desc = remapParams(targetMethod.desc); + if (isGregorianCalendarBooleanConstructor(targetMethod)) { + replaceGregorianCalendarBooleanConstructor(instructions, targetMethod); + } else if (shouldIntercept(targetMethod)) { + interceptInvokeVirtualMethod(instructions, targetMethod); + } + break; + + case INVOKEDYNAMIC: + /* no unusual behavior */ + break; + + default: + break; + } + } + } + + /** + * Verifies if the @targetMethod is a <init>(boolean) constructor for {@link java.util.GregorianCalendar} + */ + private boolean isGregorianCalendarBooleanConstructor(MethodInsnNode targetMethod) { + return targetMethod.owner.equals("java/util/GregorianCalendar") && + targetMethod.name.equals("<init>") && + targetMethod.desc.equals("(Z)V"); + } + + /** + * Replaces the void <init> (boolean) constructor for a call to the void <init> (int, int, int) one + */ + private void replaceGregorianCalendarBooleanConstructor(ListIterator<AbstractInsnNode> instructions, MethodInsnNode targetMethod) { + // Remove the call to GregorianCalendar(boolean) + instructions.remove(); + + // Discard the already-pushed parameter for GregorianCalendar(boolean) + instructions.add(new InsnNode(POP)); + + // Add parameters values for calling GregorianCalendar(int, int, int) + instructions.add(new InsnNode(ICONST_0)); + instructions.add(new InsnNode(ICONST_0)); + instructions.add(new InsnNode(ICONST_0)); + + // Call GregorianCalendar(int, int, int) + instructions.add(new MethodInsnNode(INVOKESPECIAL, targetMethod.owner, targetMethod.name, "(III)V", targetMethod.itf)); + } + + /** + * Decides to call through the appropriate method to intercept the method with an INVOKEVIRTUAL Opcode, + * depending if the invokedynamic bytecode instruction is available (Java 7+) + */ + abstract protected void interceptInvokeVirtualMethod(ListIterator<AbstractInsnNode> instructions, MethodInsnNode targetMethod); + + /** + * Replaces protected and private class modifiers with public + */ + private void makeClassPublic(ClassNode clazz) { + clazz.access = (clazz.access | ACC_PUBLIC) & ~(ACC_PROTECTED | ACC_PRIVATE); + } + + /** + * Replaces protected and private method modifiers with public + */ + private void makeMethodPublic(MethodNode method) { + method.access = (method.access | ACC_PUBLIC) & ~(ACC_PROTECTED | ACC_PRIVATE); + } + + /** + * Replaces protected and public class modifiers with private + */ + private void makeMethodPrivate(MethodNode method) { + method.access = (method.access | ACC_PRIVATE) & ~(ACC_PUBLIC | ACC_PROTECTED); + } + + private MethodNode generateStaticInitializerNotifierMethod() { + MethodNode methodNode = new MethodNode(ACC_STATIC, "<clinit>", "()V", "()V", null); + RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(methodNode); + generator.push(classType); + generator.invokeStatic(Type.getType(RobolectricInternals.class), new Method("classInitializing", "(Ljava/lang/Class;)V")); + generator.returnValue(); + generator.endMethod(); + return methodNode; + } + + // todo javadocs + protected abstract void generateShadowCall(MethodNode originalMethod, String originalMethodName, RobolectricGeneratorAdapter generator); + + int getTag(MethodNode m) { + return Modifier.isStatic(m.access) ? H_INVOKESTATIC : H_INVOKESPECIAL; + } + } + + /** + * ClassWriter implementation that verifies classes by comparing type information obtained + * from loading the classes as resources. This was taken from the ASM ClassWriter unit tests. + */ + private class InstrumentingClassWriter extends ClassWriter { + + /** + * Preserve stack map frames for V51 and newer bytecode. This fixes class verification errors + * for JDK7 and JDK8. The option to disable bytecode verification was removed in JDK8. + * + * Don't bother for V50 and earlier bytecode, because it doesn't contain stack map frames, and + * also because ASM's stack map frame handling doesn't support the JSR and RET instructions + * present in legacy bytecode. + */ + public InstrumentingClassWriter(ClassNode classNode) { + super(classNode.version >= 51 ? ClassWriter.COMPUTE_FRAMES : ClassWriter.COMPUTE_MAXS); + } + + @Override + public int newNameType(String name, String desc) { + return super.newNameType(name, desc.charAt(0) == ')' ? remapParams(desc) : remapParamType(desc)); + } + + @Override + public int newClass(String value) { + value = remapType(value); + return super.newClass(value); + } + + @Override + protected String getCommonSuperClass(final String type1, final String type2) { + try { + ClassReader info1 = typeInfo(type1); + ClassReader info2 = typeInfo(type2); + if ((info1.getAccess() & Opcodes.ACC_INTERFACE) != 0) { + if (typeImplements(type2, info2, type1)) { + return type1; + } + if ((info2.getAccess() & Opcodes.ACC_INTERFACE) != 0) { + if (typeImplements(type1, info1, type2)) { + return type2; + } + } + return "java/lang/Object"; + } + if ((info2.getAccess() & Opcodes.ACC_INTERFACE) != 0) { + if (typeImplements(type1, info1, type2)) { + return type2; + } else { + return "java/lang/Object"; + } + } + StringBuilder b1 = typeAncestors(type1, info1); + StringBuilder b2 = typeAncestors(type2, info2); + String result = "java/lang/Object"; + int end1 = b1.length(); + int end2 = b2.length(); + while (true) { + int start1 = b1.lastIndexOf(";", end1 - 1); + int start2 = b2.lastIndexOf(";", end2 - 1); + if (start1 != -1 && start2 != -1 + && end1 - start1 == end2 - start2) { + String p1 = b1.substring(start1 + 1, end1); + String p2 = b2.substring(start2 + 1, end2); + if (p1.equals(p2)) { + result = p1; + end1 = start1; + end2 = start2; + } else { + return result; + } + } else { + return result; + } + } + } catch (IOException e) { + return "java/lang/Object"; // Handle classes that may be obfuscated + } + } + + private StringBuilder typeAncestors(String type, ClassReader info) throws IOException { + StringBuilder b = new StringBuilder(); + while (!"java/lang/Object".equals(type)) { + b.append(';').append(type); + type = info.getSuperName(); + info = typeInfo(type); + } + return b; + } + + private boolean typeImplements(String type, ClassReader info, String itf) throws IOException { + while (!"java/lang/Object".equals(type)) { + String[] itfs = info.getInterfaces(); + for (String itf2 : itfs) { + if (itf2.equals(itf)) { + return true; + } + } + for (String itf1 : itfs) { + if (typeImplements(itf1, typeInfo(itf1), itf)) { + return true; + } + } + type = info.getSuperName(); + info = typeInfo(type); + } + return false; + } + + private ClassReader typeInfo(final String type) throws IOException { + try (InputStream is = getClassBytesAsStreamPreferringLocalUrls(type + ".class")) { + return new ClassReader(is); + } + } + } + + /** + * GeneratorAdapter implementation specific to generate code for Robolectric purposes + */ + private static class RobolectricGeneratorAdapter extends GeneratorAdapter { + private final boolean isStatic; + private final String desc; + + public RobolectricGeneratorAdapter(MethodNode methodNode) { + super(Opcodes.ASM4, methodNode, methodNode.access, methodNode.name, methodNode.desc); + this.isStatic = Modifier.isStatic(methodNode.access); + this.desc = methodNode.desc; + } + + public void loadThisOrNull() { + if (isStatic) { + loadNull(); + } else { + loadThis(); + } + } + + public boolean isStatic() { + return isStatic; + } + + public void loadNull() { + visitInsn(ACONST_NULL); + } + + public Type getReturnType() { + return Type.getReturnType(desc); + } + + /** + * Forces a return of a default value, depending on the method's return type + * @param type The method's return type + */ + public void pushDefaultReturnValueToStack(Type type) { + if (type.equals(Type.BOOLEAN_TYPE)) { + push(false); + } else if (type.equals(Type.INT_TYPE) || type.equals(Type.SHORT_TYPE) || type.equals(Type.BYTE_TYPE) || type.equals(Type.CHAR_TYPE)) { + push(0); + } else if (type.equals(Type.LONG_TYPE)) { + push(0l); + } else if (type.equals(Type.FLOAT_TYPE)) { + push(0f); + } else if (type.equals(Type.DOUBLE_TYPE)) { + push(0d); + } else if (type.getSort() == ARRAY || type.getSort() == OBJECT) { + loadNull(); + } + } + + private void invokeMethod(String internalClassName, MethodNode method) { + invokeMethod(internalClassName, method.name, method.desc); + } + + private void invokeMethod(String internalClassName, String methodName, String methodDesc) { + if (isStatic()) { + loadArgs(); // this, [args] + visitMethodInsn(INVOKESTATIC, internalClassName, methodName, methodDesc); + } else { + loadThisOrNull(); // this + loadArgs(); // this, [args] + visitMethodInsn(INVOKESPECIAL, internalClassName, methodName, methodDesc); + } + } + + public TryCatch tryStart(Type exceptionType) { + return new TryCatch(this, exceptionType); + } + } + + /** + * Provides try/catch code generation with a {@link org.objectweb.asm.commons.GeneratorAdapter} + */ + public static class TryCatch { + private final Label start; + private final Label end; + private final Label handler; + private final GeneratorAdapter generatorAdapter; + + public TryCatch(GeneratorAdapter generatorAdapter, Type type) { + this.generatorAdapter = generatorAdapter; + this.start = generatorAdapter.mark(); + this.end = new Label(); + this.handler = new Label(); + generatorAdapter.visitTryCatchBlock(start, end, handler, type.getInternalName()); + } + + public void end() { + generatorAdapter.mark(end); + } + + public void handler() { + generatorAdapter.mark(handler); + } + } + + private static class MissingClassMarker { + } + + public class OldClassInstrumentor extends InstrumentingClassLoader.ClassInstrumentor { + private final Type PLAN_TYPE = Type.getType(ClassHandler.Plan.class); + private final Type THROWABLE_TYPE = Type.getType(Throwable.class); + private final Method INITIALIZING_METHOD = new Method("initializing", "(Ljava/lang/Object;)Ljava/lang/Object;"); + private final Method METHOD_INVOKED_METHOD = new Method("methodInvoked", "(Ljava/lang/String;ZLjava/lang/Class;)L" + PLAN_TYPE.getInternalName() + ";"); + private final Method PLAN_RUN_METHOD = new Method("run", OBJECT_TYPE, new Type[]{OBJECT_TYPE, OBJECT_TYPE, Type.getType(Object[].class)}); + private final Method HANDLE_EXCEPTION_METHOD = new Method("cleanStackTrace", THROWABLE_TYPE, new Type[]{THROWABLE_TYPE}); + private final String DIRECT_OBJECT_MARKER_TYPE_DESC = Type.getObjectType(DirectObjectMarker.class.getName().replace('.', '/')).getDescriptor(); + private final Type ROBOLECTRIC_INTERNALS_TYPE = Type.getType(RobolectricInternals.class); + + public OldClassInstrumentor(ClassNode classNode, boolean containsStubs) { + super(classNode, containsStubs); + } + + @Override + protected void addDirectCallConstructor() { + MethodNode directCallConstructor = new MethodNode(ACC_PUBLIC, + "<init>", "(" + DIRECT_OBJECT_MARKER_TYPE_DESC + classType.getDescriptor() + ")V", null, null); + RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(directCallConstructor); + generator.loadThis(); + if (classNode.superName.equals("java/lang/Object")) { + generator.visitMethodInsn(INVOKESPECIAL, classNode.superName, "<init>", "()V"); + } else { + generator.loadArgs(); + generator.visitMethodInsn(INVOKESPECIAL, classNode.superName, + "<init>", "(" + DIRECT_OBJECT_MARKER_TYPE_DESC + "L" + classNode.superName + ";)V"); + } + generator.loadThis(); + generator.loadArg(1); + generator.putField(classType, ShadowConstants.CLASS_HANDLER_DATA_FIELD_NAME, OBJECT_TYPE); + generator.returnValue(); + classNode.methods.add(directCallConstructor); + } + + @Override + protected void writeCallToInitializing(RobolectricGeneratorAdapter generator) { + generator.invokeStatic(ROBOLECTRIC_INTERNALS_TYPE, INITIALIZING_METHOD); + } + + @Override + protected void generateShadowCall(MethodNode originalMethod, String originalMethodName, RobolectricGeneratorAdapter generator) { + generateCallToClassHandler(originalMethod, originalMethodName, generator); + } + + //TODO clean up & javadocs + private void generateCallToClassHandler(MethodNode originalMethod, String originalMethodName, RobolectricGeneratorAdapter generator) { + int planLocalVar = generator.newLocal(PLAN_TYPE); + int exceptionLocalVar = generator.newLocal(THROWABLE_TYPE); + Label directCall = new Label(); + Label doReturn = new Label(); + + boolean isNormalInstanceMethod = !generator.isStatic && !originalMethodName.equals(ShadowConstants.CONSTRUCTOR_METHOD_NAME); + + // maybe perform proxy call... + if (isNormalInstanceMethod) { + Label notInstanceOfThis = new Label(); + + generator.loadThis(); // this + generator.getField(classType, ShadowConstants.CLASS_HANDLER_DATA_FIELD_NAME, OBJECT_TYPE); // contents of __robo_data__ + generator.instanceOf(classType); // __robo_data__, is instance of same class? + generator.visitJumpInsn(IFEQ, notInstanceOfThis); // jump if no (is not instance) + + TryCatch tryCatchForProxyCall = generator.tryStart(THROWABLE_TYPE); + generator.loadThis(); // this + generator.getField(classType, ShadowConstants.CLASS_HANDLER_DATA_FIELD_NAME, OBJECT_TYPE); // contents of __robo_data__ + generator.checkCast(classType); // __robo_data__ but cast to my class + generator.loadArgs(); // __robo_data__ instance, [args] + + generator.visitMethodInsn(INVOKESPECIAL, internalClassName, originalMethod.name, originalMethod.desc); + tryCatchForProxyCall.end(); + + generator.returnValue(); + + // catch(Throwable) + tryCatchForProxyCall.handler(); + generator.storeLocal(exceptionLocalVar); + generator.loadLocal(exceptionLocalVar); + generator.invokeStatic(ROBOLECTRIC_INTERNALS_TYPE, HANDLE_EXCEPTION_METHOD); + generator.throwException(); + + // callClassHandler... + generator.mark(notInstanceOfThis); + } + + // prepare for call to classHandler.methodInvoked(String signature, boolean isStatic) + generator.push(classType.getInternalName() + "/" + originalMethodName + originalMethod.desc); + generator.push(generator.isStatic()); + generator.push(classType); // my class + generator.invokeStatic(ROBOLECTRIC_INTERNALS_TYPE, METHOD_INVOKED_METHOD); + generator.storeLocal(planLocalVar); + + generator.loadLocal(planLocalVar); // plan + generator.ifNull(directCall); + + // prepare for call to plan.run(Object instance, Object[] params) + TryCatch tryCatchForHandler = generator.tryStart(THROWABLE_TYPE); + generator.loadLocal(planLocalVar); // plan + generator.loadThisOrNull(); // instance + if (generator.isStatic()) { // roboData + generator.loadNull(); + } else { + generator.loadThis(); + generator.invokeVirtual(classType, new Method(ShadowConstants.GET_ROBO_DATA_METHOD_NAME, GET_ROBO_DATA_SIGNATURE)); + } + generator.loadArgArray(); // params + generator.invokeInterface(PLAN_TYPE, PLAN_RUN_METHOD); + + Type returnType = generator.getReturnType(); + int sort = returnType.getSort(); + switch (sort) { + case VOID: + generator.pop(); + break; + case OBJECT: + /* falls through */ + case ARRAY: + generator.checkCast(returnType); + break; + default: + int unboxLocalVar = generator.newLocal(OBJECT_TYPE); + generator.storeLocal(unboxLocalVar); + generator.loadLocal(unboxLocalVar); + Label notNull = generator.newLabel(); + Label afterward = generator.newLabel(); + generator.ifNonNull(notNull); + generator.pushDefaultReturnValueToStack(returnType); // return zero, false, whatever + generator.goTo(afterward); + + generator.mark(notNull); + generator.loadLocal(unboxLocalVar); + generator.unbox(returnType); + generator.mark(afterward); + break; + } + tryCatchForHandler.end(); + generator.goTo(doReturn); + + // catch(Throwable) + tryCatchForHandler.handler(); + generator.storeLocal(exceptionLocalVar); + generator.loadLocal(exceptionLocalVar); + generator.invokeStatic(ROBOLECTRIC_INTERNALS_TYPE, HANDLE_EXCEPTION_METHOD); + generator.throwException(); + + + if (!originalMethod.name.equals("<init>")) { + generator.mark(directCall); + TryCatch tryCatchForDirect = generator.tryStart(THROWABLE_TYPE); + generator.invokeMethod(classType.getInternalName(), originalMethod.name, originalMethod.desc); + tryCatchForDirect.end(); + generator.returnValue(); + + // catch(Throwable) + tryCatchForDirect.handler(); + generator.storeLocal(exceptionLocalVar); + generator.loadLocal(exceptionLocalVar); + generator.invokeStatic(ROBOLECTRIC_INTERNALS_TYPE, HANDLE_EXCEPTION_METHOD); + generator.throwException(); + } + + generator.mark(doReturn); + generator.returnValue(); + } + + /** + * Decides to call through the appropriate method to intercept the method with an INVOKEVIRTUAL Opcode, + * depending if the invokedynamic bytecode instruction is available (Java 7+) + */ + @Override + protected void interceptInvokeVirtualMethod(ListIterator<AbstractInsnNode> instructions, MethodInsnNode targetMethod) { + interceptInvokeVirtualMethodWithoutInvokeDynamic(instructions, targetMethod); + } + + /** + * Intercepts the method without using the invokedynamic bytecode instruction. + * Should be called through interceptInvokeVirtualMethod, not directly + */ + private void interceptInvokeVirtualMethodWithoutInvokeDynamic(ListIterator<AbstractInsnNode> instructions, MethodInsnNode targetMethod) { + boolean isStatic = targetMethod.getOpcode() == INVOKESTATIC; + + instructions.remove(); // remove the method invocation + + Type[] argumentTypes = Type.getArgumentTypes(targetMethod.desc); + + instructions.add(new LdcInsnNode(argumentTypes.length)); + instructions.add(new TypeInsnNode(ANEWARRAY, "java/lang/Object")); + + // first, move any arguments into an Object[] in reverse order + for (int i = argumentTypes.length - 1; i >= 0 ; i--) { + Type type = argumentTypes[i]; + int argWidth = type.getSize(); + + if (argWidth == 1) { // A B C [] + instructions.add(new InsnNode(DUP_X1)); // A B [] C [] + instructions.add(new InsnNode(SWAP)); // A B [] [] C + instructions.add(new LdcInsnNode(i)); // A B [] [] C 2 + instructions.add(new InsnNode(SWAP)); // A B [] [] 2 C + box(type, instructions); // A B [] [] 2 (C) + instructions.add(new InsnNode(AASTORE)); // A B [(C)] + } else if (argWidth == 2) { // A B _C_ [] + instructions.add(new InsnNode(DUP_X2)); // A B [] _C_ [] + instructions.add(new InsnNode(DUP_X2)); // A B [] [] _C_ [] + instructions.add(new InsnNode(POP)); // A B [] [] _C_ + box(type, instructions); // A B [] [] (C) + instructions.add(new LdcInsnNode(i)); // A B [] [] (C) 2 + instructions.add(new InsnNode(SWAP)); // A B [] [] 2 (C) + instructions.add(new InsnNode(AASTORE)); // A B [(C)] + } + } + + if (isStatic) { // [] + instructions.add(new InsnNode(Opcodes.ACONST_NULL)); // [] null + instructions.add(new InsnNode(Opcodes.SWAP)); // null [] + } + + // instance [] + instructions.add(new LdcInsnNode(targetMethod.owner + "/" + targetMethod.name + targetMethod.desc)); // target method signature + // instance [] signature + instructions.add(new InsnNode(DUP_X2)); // signature instance [] signature + instructions.add(new InsnNode(POP)); // signature instance [] + + instructions.add(new LdcInsnNode(classType)); // signature instance [] class + instructions.add(new MethodInsnNode(INVOKESTATIC, + Type.getType(RobolectricInternals.class).getInternalName(), "intercept", + "(Ljava/lang/String;Ljava/lang/Object;[Ljava/lang/Object;Ljava/lang/Class;)Ljava/lang/Object;")); + + final Type returnType = Type.getReturnType(targetMethod.desc); + switch (returnType.getSort()) { + case ARRAY: + /* falls through */ + case OBJECT: + instructions.add(new TypeInsnNode(CHECKCAST, remapType(returnType.getInternalName()))); + break; + case VOID: + instructions.add(new InsnNode(POP)); + break; + case Type.LONG: + instructions.add(new TypeInsnNode(CHECKCAST, Type.getInternalName(Long.class))); + instructions.add(new MethodInsnNode(INVOKEVIRTUAL, Type.getInternalName(Long.class), "longValue", Type.getMethodDescriptor(Type.LONG_TYPE), false)); + break; + case Type.FLOAT: + instructions.add(new TypeInsnNode(CHECKCAST, Type.getInternalName(Float.class))); + instructions.add(new MethodInsnNode(INVOKEVIRTUAL, Type.getInternalName(Float.class), "floatValue", Type.getMethodDescriptor(Type.FLOAT_TYPE), false)); + break; + case Type.DOUBLE: + instructions.add(new TypeInsnNode(CHECKCAST, Type.getInternalName(Double.class))); + instructions.add(new MethodInsnNode(INVOKEVIRTUAL, Type.getInternalName(Double.class), "doubleValue", Type.getMethodDescriptor(Type.DOUBLE_TYPE), false)); + break; + case Type.BOOLEAN: + instructions.add(new TypeInsnNode(CHECKCAST, Type.getInternalName(Boolean.class))); + instructions.add(new MethodInsnNode(INVOKEVIRTUAL, Type.getInternalName(Boolean.class), "booleanValue", Type.getMethodDescriptor(Type.BOOLEAN_TYPE), false)); + break; + case Type.INT: + instructions.add(new TypeInsnNode(CHECKCAST, Type.getInternalName(Integer.class))); + instructions.add(new MethodInsnNode(INVOKEVIRTUAL, Type.getInternalName(Integer.class), "intValue", Type.getMethodDescriptor(Type.INT_TYPE), false)); + break; + case Type.SHORT: + instructions.add(new TypeInsnNode(CHECKCAST, Type.getInternalName(Short.class))); + instructions.add(new MethodInsnNode(INVOKEVIRTUAL, Type.getInternalName(Short.class), "shortValue", Type.getMethodDescriptor(Type.SHORT_TYPE), false)); + break; + case Type.BYTE: + instructions.add(new TypeInsnNode(CHECKCAST, Type.getInternalName(Byte.class))); + instructions.add(new MethodInsnNode(INVOKEVIRTUAL, Type.getInternalName(Byte.class), "byteValue", Type.getMethodDescriptor(Type.BYTE_TYPE), false)); + break; + default: + throw new RuntimeException("Not implemented: " + getClass().getName() + " cannot intercept methods with return type " + returnType.getClassName()); + } + } + } + + public class InvokeDynamicClassInstrumentor extends InstrumentingClassLoader.ClassInstrumentor { + private final Handle BOOTSTRAP_INIT; + private final Handle BOOTSTRAP; + private final Handle BOOTSTRAP_STATIC; + private final Handle BOOTSTRAP_INTRINSIC; + + public InvokeDynamicClassInstrumentor(ClassNode classNode, boolean containsStubs) { + super(classNode, containsStubs); + + String className = Type.getInternalName(InvokeDynamicSupport.class); + + MethodType bootstrap = + methodType(CallSite.class, MethodHandles.Lookup.class, String.class, MethodType.class); + String bootstrapMethod = + bootstrap.appendParameterTypes(MethodHandle.class).toMethodDescriptorString(); + String bootstrapIntrinsic = + bootstrap.appendParameterTypes(String.class).toMethodDescriptorString(); + + BOOTSTRAP_INIT = new Handle(H_INVOKESTATIC, className, "bootstrapInit", bootstrap.toMethodDescriptorString()); + BOOTSTRAP = new Handle(H_INVOKESTATIC, className, "bootstrap", bootstrapMethod); + BOOTSTRAP_STATIC = new Handle(H_INVOKESTATIC, className, "bootstrapStatic", bootstrapMethod); + BOOTSTRAP_INTRINSIC = new Handle(H_INVOKESTATIC, className, "bootstrapIntrinsic", bootstrapIntrinsic); + } + + @Override + protected void addDirectCallConstructor() { + // not needed, for reasons. + } + + @Override + protected void writeCallToInitializing(RobolectricGeneratorAdapter generator) { + generator.invokeDynamic("initializing", Type.getMethodDescriptor(OBJECT_TYPE, classType), BOOTSTRAP_INIT); + } + + @Override + protected void generateShadowCall(MethodNode originalMethod, String originalMethodName, RobolectricGeneratorAdapter generator) { + generateInvokeDynamic(originalMethod, originalMethodName, generator); + } + + // todo javadocs + private void generateInvokeDynamic(MethodNode originalMethod, String originalMethodName, RobolectricGeneratorAdapter generator) { + Handle original = + new Handle(getTag(originalMethod), classType.getInternalName(), originalMethod.name, + originalMethod.desc); + + if (generator.isStatic()) { + generator.loadArgs(); + generator.invokeDynamic(originalMethodName, originalMethod.desc, BOOTSTRAP_STATIC, original); + } else { + String desc = "(" + classType.getDescriptor() + originalMethod.desc.substring(1); + generator.loadThis(); + generator.loadArgs(); + generator.invokeDynamic(originalMethodName, desc, BOOTSTRAP, original); + } + + generator.returnValue(); + } + + @Override + protected void interceptInvokeVirtualMethod(ListIterator<AbstractInsnNode> instructions, MethodInsnNode targetMethod) { + interceptInvokeVirtualMethodWithInvokeDynamic(instructions, targetMethod); + } + + /** + * Intercepts the method using the invokedynamic bytecode instruction available in Java 7+. + * Should be called through interceptInvokeVirtualMethod, not directly + */ + private void interceptInvokeVirtualMethodWithInvokeDynamic(ListIterator<AbstractInsnNode> instructions, MethodInsnNode targetMethod) { + instructions.remove(); // remove the method invocation + + Type type = Type.getObjectType(targetMethod.owner); + String description = targetMethod.desc; + String owner = type.getClassName(); + + if (targetMethod.getOpcode() != INVOKESTATIC) { + String thisType = type.getDescriptor(); + description = "(" + thisType + description.substring(1, description.length()); + } + + instructions.add(new InvokeDynamicInsnNode(targetMethod.name, description, BOOTSTRAP_INTRINSIC, owner)); + } + } +} diff --git a/robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/Interceptor.java b/robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/Interceptor.java new file mode 100644 index 000000000..2125c255c --- /dev/null +++ b/robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/Interceptor.java @@ -0,0 +1,34 @@ +package org.robolectric.internal.bytecode; + +import org.jetbrains.annotations.NotNull; +import org.robolectric.util.Function; +import org.robolectric.util.ReflectionHelpers; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodType; + +public abstract class Interceptor { + private MethodRef[] methodRefs; + + public Interceptor(MethodRef... methodRefs) { + this.methodRefs = methodRefs; + } + + public MethodRef[] getMethodRefs() { + return methodRefs; + } + + abstract public Function<Object, Object> handle(MethodSignature methodSignature); + + abstract public MethodHandle getMethodHandle(String methodName, MethodType type) throws NoSuchMethodException, IllegalAccessException; + + @NotNull + protected static Function<Object, Object> returnDefaultValue(final MethodSignature methodSignature) { + return new Function<Object, Object>() { + @Override + public Object call(Class<?> theClass, Object value, Object[] params) { + return ReflectionHelpers.defaultValueForType(methodSignature.returnType); + } + }; + } +} diff --git a/robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/Interceptors.java b/robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/Interceptors.java new file mode 100644 index 000000000..adcbc9d8e --- /dev/null +++ b/robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/Interceptors.java @@ -0,0 +1,47 @@ +package org.robolectric.internal.bytecode; + +import org.robolectric.util.Function; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import static java.util.Arrays.asList; + +public class Interceptors { + private final Map<MethodRef, Interceptor> interceptors = new HashMap<>(); + + public Interceptors(Interceptor... interceptors) { + this(asList(interceptors)); + } + + public Interceptors(Collection<Interceptor> interceptorList) { + for (Interceptor interceptor : interceptorList) { + for (MethodRef methodRef : interceptor.getMethodRefs()) { + this.interceptors.put(methodRef, interceptor); + } + } + } + + public Collection<MethodRef> getAllMethodRefs() { + return interceptors.keySet(); + } + + public Function<Object, Object> getInterceptionHandler(final MethodSignature methodSignature) { + Interceptor interceptor = findInterceptor(methodSignature.className, methodSignature.methodName); + if (interceptor != null) { + return interceptor.handle(methodSignature); + } + + // nothing matched, return default + return Interceptor.returnDefaultValue(methodSignature); + } + + public Interceptor findInterceptor(String className, String methodName) { + Interceptor mh = interceptors.get(new MethodRef(className, methodName)); + if (mh == null) { + mh = interceptors.get(new MethodRef(className, "*")); + } + return mh; + } +} diff --git a/robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/InvocationProfile.java b/robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/InvocationProfile.java new file mode 100644 index 000000000..88f6e99a1 --- /dev/null +++ b/robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/InvocationProfile.java @@ -0,0 +1,70 @@ +package org.robolectric.internal.bytecode; + +import java.util.Arrays; + +public class InvocationProfile { + public final Class clazz; + public final String methodName; + public final boolean isStatic; + public final String[] paramTypes; + private final boolean isDeclaredOnObject; + + public InvocationProfile(String methodSignatureString, boolean isStatic, ClassLoader classLoader) { + MethodSignature methodSignature = MethodSignature.parse(methodSignatureString); + this.clazz = loadClass(classLoader, methodSignature.className); + this.methodName = methodSignature.methodName; + this.paramTypes = methodSignature.paramTypes; + this.isStatic = isStatic; + + this.isDeclaredOnObject = methodSignatureString.endsWith("/equals(Ljava/lang/Object;)Z") + || methodSignatureString.endsWith("/hashCode()I") + || methodSignatureString.endsWith("/toString()Ljava/lang/String;"); + } + + public Class<?>[] getParamClasses(ClassLoader classLoader) throws ClassNotFoundException { + Class[] classes = new Class[paramTypes.length]; + for (int i = 0; i < paramTypes.length; i++) { + String paramType = paramTypes[i]; + classes[i] = ShadowWrangler.loadClass(paramType, classLoader); + } + return classes; + } + + private Class<?> loadClass(ClassLoader classLoader, String className) { + try { + return classLoader.loadClass(className); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + InvocationProfile that = (InvocationProfile) o; + + if (isDeclaredOnObject != that.isDeclaredOnObject) return false; + if (isStatic != that.isStatic) return false; + if (clazz != null ? !clazz.equals(that.clazz) : that.clazz != null) return false; + if (methodName != null ? !methodName.equals(that.methodName) : that.methodName != null) return false; + if (!Arrays.equals(paramTypes, that.paramTypes)) return false; + + return true; + } + + @Override + public int hashCode() { + int result = clazz != null ? clazz.hashCode() : 0; + result = 31 * result + (methodName != null ? methodName.hashCode() : 0); + result = 31 * result + (isStatic ? 1 : 0); + result = 31 * result + (paramTypes != null ? Arrays.hashCode(paramTypes) : 0); + result = 31 * result + (isDeclaredOnObject ? 1 : 0); + return result; + } + + public boolean isDeclaredOnObject() { + return isDeclaredOnObject; + } +} diff --git a/robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/InvokeDynamicSupport.java b/robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/InvokeDynamicSupport.java new file mode 100644 index 000000000..9e5489027 --- /dev/null +++ b/robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/InvokeDynamicSupport.java @@ -0,0 +1,165 @@ +package org.robolectric.internal.bytecode; + +import org.robolectric.internal.ShadowedObject; +import org.robolectric.util.ReflectionHelpers; + +import java.lang.invoke.CallSite; +import java.lang.invoke.ConstantCallSite; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.invoke.SwitchPoint; + +import static java.lang.invoke.MethodHandles.catchException; +import static java.lang.invoke.MethodHandles.constant; +import static java.lang.invoke.MethodHandles.dropArguments; +import static java.lang.invoke.MethodHandles.exactInvoker; +import static java.lang.invoke.MethodHandles.filterArguments; +import static java.lang.invoke.MethodHandles.foldArguments; +import static java.lang.invoke.MethodHandles.throwException; +import static java.lang.invoke.MethodType.methodType; +import static org.robolectric.internal.bytecode.MethodCallSite.Kind.REGULAR; +import static org.robolectric.internal.bytecode.MethodCallSite.Kind.STATIC; + +public class InvokeDynamicSupport { + @SuppressWarnings("unused") + private static Interceptors INTERCEPTORS; + + private static final MethodHandle BIND_CALL_SITE; + private static final MethodHandle BIND_INIT_CALL_SITE; + private static final MethodHandle EXCEPTION_HANDLER; + private static final MethodHandle GET_SHADOW; + + static { + try { + MethodHandles.Lookup lookup = MethodHandles.lookup(); + + BIND_CALL_SITE = lookup.findStatic(InvokeDynamicSupport.class, "bindCallSite", + methodType(MethodHandle.class, MethodCallSite.class)); + BIND_INIT_CALL_SITE = lookup.findStatic(InvokeDynamicSupport.class, "bindInitCallSite", + methodType(MethodHandle.class, RoboCallSite.class)); + MethodHandle cleanStackTrace = lookup.findStatic(RobolectricInternals.class, "cleanStackTrace", + methodType(Throwable.class, Throwable.class)); + EXCEPTION_HANDLER = filterArguments(throwException(void.class, Throwable.class), 0, cleanStackTrace); + GET_SHADOW = lookup.findVirtual(ShadowedObject.class, "$$robo$getData", methodType(Object.class)); + } catch (NoSuchMethodException | IllegalAccessException e) { + throw new AssertionError(e); + } + } + + @SuppressWarnings("UnusedDeclaration") + public static CallSite bootstrapInit(MethodHandles.Lookup caller, String name, MethodType type) { + RoboCallSite site = new RoboCallSite(type, caller.lookupClass()); + + bindInitCallSite(site); + + return site; + } + + @SuppressWarnings("UnusedDeclaration") + public static CallSite bootstrap(MethodHandles.Lookup caller, String name, MethodType type, + MethodHandle original) throws IllegalAccessException { + MethodCallSite site = new MethodCallSite(type, caller.lookupClass(), name, original, REGULAR); + + bindCallSite(site); + + return site; + } + + @SuppressWarnings("UnusedDeclaration") + public static CallSite bootstrapStatic(MethodHandles.Lookup caller, String name, MethodType type, + MethodHandle original) throws IllegalAccessException { + MethodCallSite site = new MethodCallSite(type, caller.lookupClass(), name, original, STATIC); + + bindCallSite(site); + + return site; + } + + @SuppressWarnings("UnusedDeclaration") + public static CallSite bootstrapIntrinsic(MethodHandles.Lookup caller, String name, + MethodType type, String callee) throws IllegalAccessException { + + MethodHandle mh = getMethodHandle(callee, name, type); + if (mh == null) { + throw new IllegalArgumentException("Could not find intrinsic for " + callee + ":" + name); + } + + return new ConstantCallSite(mh.asType(type)); + } + + private static final MethodHandle NOTHING = constant(Void.class, null).asType(methodType(void.class)); + + private static MethodHandle getMethodHandle(String className, String methodName, MethodType type) { + Interceptor interceptor = INTERCEPTORS.findInterceptor(className, methodName); + if (interceptor != null) { + try { + // reload interceptor in sandbox... + Class<Interceptor> theClass = + (Class<Interceptor>) ReflectionHelpers.loadClass( + RobolectricInternals.getClassLoader(), + interceptor.getClass().getName()).asSubclass(Interceptor.class); + return ReflectionHelpers.newInstance(theClass).getMethodHandle(methodName, type); + } catch (NoSuchMethodException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + if (type.parameterCount() != 0) { + return dropArguments(NOTHING, 0, type.parameterArray()); + } else { + return NOTHING; + } + } + + private static MethodHandle bindInitCallSite(RoboCallSite site) { + MethodHandle mh = RobolectricInternals.getShadowCreator(site.getCaller()); + return bindWithFallback(mh, site, BIND_INIT_CALL_SITE); + } + + private static MethodHandle bindCallSite(MethodCallSite site) throws IllegalAccessException { + MethodHandle mh = + RobolectricInternals.findShadowMethod(site.getCaller(), site.getName(), site.type(), + site.isStatic()); + + if (mh == null) { + // Call original code and make sure to clean stack traces + mh = cleanStackTraces(site.getOriginal()); + } else if (mh == ShadowWrangler.DO_NOTHING) { + mh = dropArguments(mh, 0, site.type().parameterList()); + } else if (!site.isStatic()) { + Class<?> shadowType = mh.type().parameterType(0); + mh = filterArguments(mh, 0, GET_SHADOW.asType(methodType(shadowType, site.thisType()))); + } + + try { + return bindWithFallback(mh, site, BIND_CALL_SITE); + } catch (Throwable t) { + // The error that bubbles up is currently not very helpful so we print any error messages + // here + t.printStackTrace(); + System.err.println(site.getCaller()); + throw t; + } + } + + private static MethodHandle bindWithFallback(MethodHandle mh, RoboCallSite site, MethodHandle fallback) { + SwitchPoint switchPoint = getInvalidator(site.getCaller()); + MethodType type = site.type(); + + MethodHandle boundFallback = foldArguments(exactInvoker(type), fallback.bindTo(site)); + mh = switchPoint.guardWithTest(mh.asType(type), boundFallback); + + site.setTarget(mh); + return mh; + } + + private static SwitchPoint getInvalidator(Class<?> cl) { + return RobolectricInternals.getShadowInvalidator().getSwitchPoint(cl); + } + + private static MethodHandle cleanStackTraces(MethodHandle mh) { + MethodType type = EXCEPTION_HANDLER.type().changeReturnType(mh.type().returnType()); + return catchException(mh, Throwable.class, EXCEPTION_HANDLER.asType(type)); + } +} diff --git a/robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/MethodCallSite.java b/robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/MethodCallSite.java new file mode 100644 index 000000000..48643ea17 --- /dev/null +++ b/robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/MethodCallSite.java @@ -0,0 +1,50 @@ +package org.robolectric.internal.bytecode; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodType; +import java.lang.invoke.MutableCallSite; + +import static org.robolectric.internal.bytecode.MethodCallSite.Kind.STATIC; + +public class MethodCallSite extends RoboCallSite { + private final String name; + private final MethodHandle original; + private final Kind kind; + + public MethodCallSite(MethodType type, Class<?> caller, String name, MethodHandle original, + Kind kind) { + super(type, caller); + this.name = name; + this.original = original; + this.kind = kind; + } + + public String getName() { + return name; + } + + public MethodHandle getOriginal() { + return original; + } + + public Class<?> thisType() { + return isStatic() ? null : type().parameterType(0); + } + + public boolean isStatic() { + return kind == STATIC; + } + + @Override public String toString() { + return "RoboCallSite{" + + "caller=" + getCaller() + + ", original=" + original + + ", kind=" + kind + + '}'; + } + + public enum Kind { + REGULAR, + STATIC + } +} diff --git a/robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/MethodRef.java b/robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/MethodRef.java new file mode 100644 index 000000000..46c1cfb2d --- /dev/null +++ b/robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/MethodRef.java @@ -0,0 +1,42 @@ +package org.robolectric.internal.bytecode; + +/** + * Reference to a specific method on a class. + */ +public class MethodRef { + public final String className; + public final String methodName; + + public MethodRef(Class<?> clazz, String methodName) { + this(clazz.getName(), methodName); + } + + public MethodRef(String className, String methodName) { + this.className = className; + this.methodName = methodName; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + MethodRef methodRef = (MethodRef) o; + + return className.equals(methodRef.className) && methodName.equals(methodRef.methodName); + } + + @Override public int hashCode() { + int result = className.hashCode(); + result = 31 * result + methodName.hashCode(); + return result; + } + + @Override + public String toString() { + return "MethodRef{" + + "className='" + className + '\'' + + ", methodName='" + methodName + '\'' + + '}'; + } +} diff --git a/robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/MethodSignature.java b/robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/MethodSignature.java new file mode 100644 index 000000000..afc4e2c86 --- /dev/null +++ b/robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/MethodSignature.java @@ -0,0 +1,42 @@ +package org.robolectric.internal.bytecode; + +import org.objectweb.asm.Type; +import org.robolectric.util.Join; + +public class MethodSignature { + public final String className; + public final String methodName; + public final String[] paramTypes; + public final String returnType; + + private MethodSignature(String className, String methodName, String[] paramTypes, String returnType) { + this.className = className; + this.methodName = methodName; + this.paramTypes = paramTypes; + this.returnType = returnType; + } + + public static MethodSignature parse(String internalString) { + int parenStart = internalString.indexOf('('); + int methodStart = internalString.lastIndexOf('/', parenStart); + String className = internalString.substring(0, methodStart).replace('/', '.'); + String methodName = internalString.substring(methodStart + 1, parenStart); + String methodDescriptor = internalString.substring(parenStart); + Type[] argumentTypes = Type.getArgumentTypes(methodDescriptor); + String[] paramTypes = new String[argumentTypes.length]; + for (int i = 0; i < argumentTypes.length; i++) { + paramTypes[i] = argumentTypes[i].getClassName(); + } + final String returnType = Type.getReturnType(methodDescriptor).getClassName(); + return new MethodSignature(className, methodName, paramTypes, returnType); + } + + @Override + public String toString() { + return className + "." + methodName + "(" + Join.join(", ", (Object[]) paramTypes) + ")"; + } + + boolean matches(String className, String methodName) { + return this.className.equals(className) && this.methodName.equals(methodName); + } +} diff --git a/robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/RoboCallSite.java b/robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/RoboCallSite.java new file mode 100644 index 000000000..46357b056 --- /dev/null +++ b/robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/RoboCallSite.java @@ -0,0 +1,17 @@ +package org.robolectric.internal.bytecode; + +import java.lang.invoke.MethodType; +import java.lang.invoke.MutableCallSite; + +public class RoboCallSite extends MutableCallSite { + private final Class<?> caller; + + public RoboCallSite(MethodType type, Class<?> caller) { + super(type); + this.caller = caller; + } + + public Class<?> getCaller() { + return caller; + } +} diff --git a/robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/RoboConfig.java b/robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/RoboConfig.java new file mode 100644 index 000000000..a0fe463b3 --- /dev/null +++ b/robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/RoboConfig.java @@ -0,0 +1,31 @@ +package org.robolectric.internal.bytecode; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Configuration settings that can be used on a per-class or per-test basis. + */ +@Documented +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.METHOD}) +public @interface RoboConfig { + /** + * A list of shadow classes to enable, in addition to those that are already present. + * + * @return A list of additional shadow classes to enable. + */ + Class<?>[] shadows() default {}; // DEFAULT_SHADOWS + + /** + * A list of instrumented packages, in addition to those that are already instrumented. + * + * @return A list of additional instrumented packages. + */ + String[] instrumentedPackages() default {}; // DEFAULT_INSTRUMENTED_PACKAGES +} diff --git a/robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/RoboType.java b/robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/RoboType.java new file mode 100644 index 000000000..3dd8741ce --- /dev/null +++ b/robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/RoboType.java @@ -0,0 +1,29 @@ +package org.robolectric.internal.bytecode; + +enum RoboType { + VOID(Void.TYPE), + BOOLEAN(Boolean.TYPE), + BYTE(Byte.TYPE), + CHAR(Character.TYPE), + SHORT(Short.TYPE), + INT(Integer.TYPE), + LONG(Long.TYPE), + FLOAT(Float.TYPE), + DOUBLE(Double.TYPE), + OBJECT(null); + + RoboType(Class type) { + this.type = type; + } + + private Class type; + + public static Class findPrimitiveClass(String name) { + for (RoboType type : RoboType.values()) { + if (type.type != null && type.type.getName().equals(name)) { + return type.type; + } + } + return null; + } +} diff --git a/robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/RobolectricInternals.java b/robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/RobolectricInternals.java new file mode 100644 index 000000000..05f1e92c3 --- /dev/null +++ b/robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/RobolectricInternals.java @@ -0,0 +1,67 @@ +package org.robolectric.internal.bytecode; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodType; +import org.robolectric.util.ReflectionHelpers; +import org.robolectric.internal.ShadowConstants; + +public class RobolectricInternals { + + @SuppressWarnings("UnusedDeclaration") + private static ClassHandler classHandler; // initialized via magic by SdkEnvironment + + @SuppressWarnings("UnusedDeclaration") + private static ShadowInvalidator shadowInvalidator; + + @SuppressWarnings("UnusedDeclaration") + private static InstrumentingClassLoader classLoader; + + @SuppressWarnings("UnusedDeclaration") + public static void classInitializing(Class clazz) throws Exception { + classHandler.classInitializing(clazz); + } + + @SuppressWarnings("UnusedDeclaration") + public static Object initializing(Object instance) throws Exception { + return classHandler.initializing(instance); + } + + @SuppressWarnings("UnusedDeclaration") + public static ClassHandler.Plan methodInvoked(String signature, boolean isStatic, Class<?> theClass) { + return classHandler.methodInvoked(signature, isStatic, theClass); + } + + public static MethodHandle getShadowCreator(Class<?> caller) { + return classHandler.getShadowCreator(caller); + } + + public static MethodHandle findShadowMethod(Class<?> theClass, String name, + MethodType type, boolean isStatic) throws IllegalAccessException { + return classHandler.findShadowMethod(theClass, name, type, isStatic); + } + + @SuppressWarnings("UnusedDeclaration") + public static Throwable cleanStackTrace(Throwable exception) { + return classHandler.stripStackTrace(exception); + } + + public static Object intercept(String signature, Object instance, Object[] params, Class theClass) throws Throwable { + try { + return classHandler.intercept(signature, instance, params, theClass); + } catch (java.lang.LinkageError e) { + throw new Exception(e); + } + } + + public static void performStaticInitialization(Class<?> clazz) { + ReflectionHelpers.callStaticMethod(clazz, ShadowConstants.STATIC_INITIALIZER_METHOD_NAME); + } + + public static ShadowInvalidator getShadowInvalidator() { + return shadowInvalidator; + } + + public static InstrumentingClassLoader getClassLoader() { + return classLoader; + } +} diff --git a/robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/Sandbox.java b/robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/Sandbox.java new file mode 100644 index 000000000..95c99c2cd --- /dev/null +++ b/robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/Sandbox.java @@ -0,0 +1,68 @@ +package org.robolectric.internal.bytecode; + +import org.robolectric.internal.InvokeDynamic; +import org.robolectric.internal.Shadow; +import org.robolectric.internal.ShadowImpl; +import org.robolectric.util.ReflectionHelpers; + +import java.util.Set; + +import static org.robolectric.util.ReflectionHelpers.newInstance; +import static org.robolectric.util.ReflectionHelpers.setStaticField; + +public class Sandbox { + private final ClassLoader robolectricClassLoader; + private final ShadowInvalidator shadowInvalidator; + public ClassHandler classHandler; // todo not public + private ShadowMap shadowMap = ShadowMap.EMPTY; + + public Sandbox(ClassLoader robolectricClassLoader) { + this.robolectricClassLoader = robolectricClassLoader; + this.shadowInvalidator = new ShadowInvalidator(); + } + + public <T> Class<T> bootstrappedClass(Class<?> testClass) { + try { + return (Class<T>) robolectricClassLoader.loadClass(testClass.getName()); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + public ClassLoader getRobolectricClassLoader() { + return robolectricClassLoader; + } + + public ShadowInvalidator getShadowInvalidator() { + return shadowInvalidator; + } + + public ClassHandler getClassHandler() { + return classHandler; + } + + public void replaceShadowMap(ShadowMap shadowMap) { + if (InvokeDynamic.ENABLED) { + ShadowMap oldShadowMap = this.shadowMap; + this.shadowMap = shadowMap; + Set<String> invalidatedClasses = shadowMap.getInvalidatedClasses(oldShadowMap); + getShadowInvalidator().invalidateClasses(invalidatedClasses); + } + } + + public void injectEnvironment(Interceptors interceptors) { + ClassLoader robolectricClassLoader = getRobolectricClassLoader(); + ClassHandler classHandler = getClassHandler(); + ShadowInvalidator invalidator = getShadowInvalidator(); + + Class<?> robolectricInternalsClass = bootstrappedClass(RobolectricInternals.class); + setStaticField(robolectricInternalsClass, "classHandler", classHandler); + setStaticField(robolectricInternalsClass, "shadowInvalidator", invalidator); + setStaticField(robolectricInternalsClass, "classLoader", robolectricClassLoader); + + Class<?> invokeDynamicSupportClass = bootstrappedClass(InvokeDynamicSupport.class); + setStaticField(invokeDynamicSupportClass, "INTERCEPTORS", interceptors); + + Class<?> shadowClass = bootstrappedClass(Shadow.class); + setStaticField(shadowClass, "SHADOW_IMPL", newInstance(bootstrappedClass(ShadowImpl.class))); + } +} diff --git a/robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/ShadowConfig.java b/robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/ShadowConfig.java new file mode 100644 index 000000000..e5374d3b5 --- /dev/null +++ b/robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/ShadowConfig.java @@ -0,0 +1,62 @@ +package org.robolectric.internal.bytecode; + +import org.robolectric.annotation.Implements; + +public class ShadowConfig { + public final String shadowClassName; + public final boolean callThroughByDefault; + public final boolean inheritImplementationMethods; + public final boolean looseSignatures; + private final int minSdk; + private final int maxSdk; + + ShadowConfig(String shadowClassName, boolean callThroughByDefault, boolean inheritImplementationMethods, + boolean looseSignatures, int minSdk, int maxSdk) { + this.shadowClassName = shadowClassName; + this.callThroughByDefault = callThroughByDefault; + this.inheritImplementationMethods = inheritImplementationMethods; + this.looseSignatures = looseSignatures; + this.minSdk = minSdk; + this.maxSdk = maxSdk; + } + + ShadowConfig(String shadowClassName, Implements annotation) { + this(shadowClassName, + annotation.callThroughByDefault(), + annotation.inheritImplementationMethods(), + annotation.looseSignatures(), + annotation.minSdk(), + annotation.maxSdk()); + } + + public boolean supportsSdk(int sdkInt) { + return minSdk <= sdkInt && (maxSdk == -1 || maxSdk >= sdkInt); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ShadowConfig that = (ShadowConfig) o; + + if (callThroughByDefault != that.callThroughByDefault) return false; + if (inheritImplementationMethods != that.inheritImplementationMethods) return false; + if (looseSignatures != that.looseSignatures) return false; + if (minSdk != that.minSdk) return false; + if (maxSdk != that.maxSdk) return false; + return shadowClassName != null ? shadowClassName.equals(that.shadowClassName) : that.shadowClassName == null; + + } + + @Override + public int hashCode() { + int result = shadowClassName != null ? shadowClassName.hashCode() : 0; + result = 31 * result + (callThroughByDefault ? 1 : 0); + result = 31 * result + (inheritImplementationMethods ? 1 : 0); + result = 31 * result + (looseSignatures ? 1 : 0); + result = 31 * result + minSdk; + result = 31 * result + maxSdk; + return result; + } +} diff --git a/robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/ShadowInvalidator.java b/robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/ShadowInvalidator.java new file mode 100644 index 000000000..6baa7e57d --- /dev/null +++ b/robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/ShadowInvalidator.java @@ -0,0 +1,43 @@ +package org.robolectric.internal.bytecode; + +import java.lang.invoke.SwitchPoint; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +public class ShadowInvalidator { + private static final SwitchPoint DUMMY = new SwitchPoint(); + + static { + SwitchPoint.invalidateAll(new SwitchPoint[] { DUMMY }); + } + + private Map<String, SwitchPoint> switchPoints; + + public ShadowInvalidator() { + this.switchPoints = new HashMap<>(); + } + + public SwitchPoint getSwitchPoint(Class<?> caller) { + return getSwitchPoint(caller.getName()); + } + + public synchronized SwitchPoint getSwitchPoint(String className) { + SwitchPoint switchPoint = switchPoints.get(className); + if (switchPoint == null) switchPoints.put(className, switchPoint = new SwitchPoint()); + return switchPoint; + } + + public synchronized void invalidateClasses(Collection<String> classNames) { + if (classNames.isEmpty()) return; + SwitchPoint[] points = new SwitchPoint[classNames.size()]; + int i = 0; + for (String className : classNames) { + SwitchPoint switchPoint = switchPoints.put(className, null); + if (switchPoint == null) switchPoint = DUMMY; + points[i++] = switchPoint; + } + + SwitchPoint.invalidateAll(points); + } +} diff --git a/robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/ShadowMap.java b/robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/ShadowMap.java new file mode 100644 index 000000000..30e74ebf0 --- /dev/null +++ b/robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/ShadowMap.java @@ -0,0 +1,197 @@ +package org.robolectric.internal.bytecode; + +import java.util.Set; +import org.robolectric.annotation.Implements; +import org.robolectric.internal.ShadowProvider; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.ServiceLoader; + +public class ShadowMap { + public static final ShadowMap EMPTY = new ShadowMap(Collections.<String, ShadowConfig>emptyMap()); + private final Map<String, ShadowConfig> map; + private static final Map<String, String> SHADOWS = new HashMap<>(); + + static { + for (ShadowProvider provider : ServiceLoader.load(ShadowProvider.class)) { + SHADOWS.putAll(provider.getShadowMap()); + } + } + + ShadowMap(Map<String, ShadowConfig> map) { + this.map = new HashMap<>(map); + } + + public ShadowConfig get(Class<?> clazz) { + ShadowConfig shadowConfig = map.get(clazz.getName()); + + if (shadowConfig == null && clazz.getClassLoader() != null) { + Class<?> shadowClass = getShadowClass(clazz); + if (shadowClass == null) { + return null; + } + + ShadowInfo shadowInfo = getShadowInfo(shadowClass); + if (shadowInfo != null && shadowInfo.shadowedClassName.equals(clazz.getName())) { + return shadowInfo.getShadowConfig(); + } + } + return shadowConfig; + } + + private static Class<?> getShadowClass(Class<?> clazz) { + try { + final String className = clazz.getCanonicalName(); + if (className != null) { + final String shadowName = SHADOWS.get(className); + if (shadowName != null) { + return clazz.getClassLoader().loadClass(shadowName); + } + } + } catch (ClassNotFoundException e) { + return null; + } catch (IncompatibleClassChangeError e) { + return null; + } + return null; + } + + public static ShadowInfo getShadowInfo(Class<?> clazz) { + 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(); + } + return new ShadowInfo(className, new ShadowConfig(clazz.getName(), annotation)); + } + + public Set<String> getInvalidatedClasses(ShadowMap previous) { + if (this == previous) return Collections.emptySet(); + + Map<String, ShadowConfig> invalidated = new HashMap<>(); + invalidated.putAll(map); + + for (Map.Entry<String, ShadowConfig> entry : previous.map.entrySet()) { + String className = entry.getKey(); + ShadowConfig previousConfig = entry.getValue(); + ShadowConfig currentConfig = invalidated.get(className); + if (currentConfig == null) { + invalidated.put(className, previousConfig); + } else if (previousConfig.equals(currentConfig)) { + invalidated.remove(className); + } + } + + return invalidated.keySet(); + } + + public static String convertToShadowName(String className) { + String shadowClassName = + "org.robolectric.shadows.Shadow" + className.substring(className.lastIndexOf(".") + 1); + shadowClassName = shadowClassName.replaceAll("\\$", "\\$Shadow"); + return shadowClassName; + } + + public Builder newBuilder() { + return new Builder(this); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ShadowMap shadowMap = (ShadowMap) o; + + if (!map.equals(shadowMap.map)) return false; + + return true; + } + + @Override + public int hashCode() { + return map.hashCode(); + } + + public static class Builder { + private final Map<String, ShadowConfig> map; + + public Builder() { + map = new HashMap<>(); + } + + public Builder(ShadowMap shadowMap) { + this.map = new HashMap<>(shadowMap.map); + } + + public Builder addShadowClasses(Class<?>... shadowClasses) { + for (Class<?> shadowClass : shadowClasses) { + addShadowClass(shadowClass); + } + return this; + } + + public Builder addShadowClasses(Collection<Class<?>> shadowClasses) { + for (Class<?> shadowClass : shadowClasses) { + addShadowClass(shadowClass); + } + return this; + } + + public Builder addShadowClass(Class<?> shadowClass) { + ShadowInfo shadowInfo = getShadowInfo(shadowClass); + if (shadowInfo != null) { + addShadowConfig(shadowInfo.getShadowedClassName(), shadowInfo.getShadowConfig()); + } + return this; + } + + public Builder addShadowClass(String realClassName, Class<?> shadowClass, boolean callThroughByDefault, boolean inheritImplementationMethods, boolean looseSignatures) { + addShadowClass(realClassName, shadowClass.getName(), callThroughByDefault, inheritImplementationMethods, looseSignatures); + return this; + } + + public Builder addShadowClass(Class<?> realClass, Class<?> shadowClass, boolean callThroughByDefault, boolean inheritImplementationMethods, boolean looseSignatures) { + addShadowClass(realClass.getName(), shadowClass.getName(), callThroughByDefault, inheritImplementationMethods, looseSignatures); + return this; + } + + public Builder addShadowClass(String realClassName, String shadowClassName, boolean callThroughByDefault, boolean inheritImplementationMethods, boolean looseSignatures) { + addShadowConfig(realClassName, new ShadowConfig(shadowClassName, callThroughByDefault, inheritImplementationMethods, looseSignatures, -1, -1)); + return this; + } + + private void addShadowConfig(String realClassName, ShadowConfig shadowConfig) { + map.put(realClassName, shadowConfig); + } + + public ShadowMap build() { + return new ShadowMap(map); + } + } + + public static class ShadowInfo { + private final String shadowedClassName; + private final ShadowConfig shadowConfig; + + ShadowInfo(String shadowedClassName, ShadowConfig shadowConfig) { + this.shadowConfig = shadowConfig; + this.shadowedClassName = shadowedClassName; + } + + public String getShadowedClassName() { + return shadowedClassName; + } + + public ShadowConfig getShadowConfig() { + return shadowConfig; + } + } +} diff --git a/robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/ShadowWrangler.java b/robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/ShadowWrangler.java new file mode 100644 index 000000000..e194906e8 --- /dev/null +++ b/robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/ShadowWrangler.java @@ -0,0 +1,493 @@ +package org.robolectric.internal.bytecode; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; + +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; +import org.robolectric.annotation.RealObject; +import org.robolectric.util.ReflectionHelpers; +import org.robolectric.util.Function; +import org.robolectric.internal.ShadowConstants; + +import java.lang.reflect.*; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +import static java.lang.invoke.MethodHandles.constant; +import static java.lang.invoke.MethodHandles.dropArguments; +import static java.lang.invoke.MethodHandles.foldArguments; +import static java.lang.invoke.MethodHandles.identity; +import static java.lang.invoke.MethodType.methodType; + +public class ShadowWrangler implements ClassHandler { + public static final Function<Object, Object> DO_NOTHING_HANDLER = new Function<Object, Object>() { + @Override + public Object call(Class<?> theClass, Object value, Object[] params) { + return null; + } + }; + public static final Plan DO_NOTHING_PLAN = new Plan() { + @Override + public Object run(Object instance, Object roboData, Object[] params) throws Exception { + return null; + } + + @Override + public String describe() { + return "do nothing"; + } + }; + public static final Plan CALL_REAL_CODE_PLAN = null; + public static final MethodHandle CALL_REAL_CODE = null; + public static final MethodHandle DO_NOTHING = constant(Void.class, null).asType(methodType(void.class)); + private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup(); + private static final boolean STRIP_SHADOW_STACK_TRACES = true; + private static final ShadowConfig NO_SHADOW_CONFIG = new ShadowConfig(Object.class.getName(), true, false, false, -1, -1); + static final Object NO_SHADOW = new Object(); + private static final MethodHandle NO_SHADOW_HANDLE = constant(Object.class, NO_SHADOW); + private final ShadowMap shadowMap; + private final Interceptors interceptors; + private final int apiLevel; + private final Map<Class, MetaShadow> metaShadowMap = new HashMap<>(); + private final Map<String, Plan> planCache = + Collections.synchronizedMap(new LinkedHashMap<String, Plan>() { + @Override + protected boolean removeEldestEntry(Map.Entry<String, Plan> eldest) { + return size() > 500; + } + }); + private final Map<Class, ShadowConfig> shadowConfigCache = new ConcurrentHashMap<>(); + private final ClassValue<ShadowConfig> shadowConfigs = new ClassValue<ShadowConfig>() { + @Override protected ShadowConfig computeValue(Class<?> type) { + return shadowMap.get(type); + } + }; + + public ShadowWrangler(ShadowMap shadowMap, int apiLevel, Interceptors interceptors) { + this.shadowMap = shadowMap; + this.apiLevel = apiLevel; + this.interceptors = interceptors; + } + + public static Class<?> loadClass(String paramType, ClassLoader classLoader) { + Class primitiveClass = RoboType.findPrimitiveClass(paramType); + if (primitiveClass != null) return primitiveClass; + + int arrayLevel = 0; + while (paramType.endsWith("[]")) { + arrayLevel++; + paramType = paramType.substring(0, paramType.length() - 2); + } + + Class<?> clazz = RoboType.findPrimitiveClass(paramType); + if (clazz == null) { + try { + clazz = classLoader.loadClass(paramType); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + + while (arrayLevel-- > 0) { + clazz = Array.newInstance(clazz, 0).getClass(); + } + + return clazz; + } + + @Override + public void classInitializing(Class clazz) { + Class<?> shadowClass = findDirectShadowClass(clazz); + if (shadowClass != null) { + try { + Method method = shadowClass.getMethod(ShadowConstants.STATIC_INITIALIZER_METHOD_NAME); + if (!Modifier.isStatic(method.getModifiers())) { + throw new RuntimeException(shadowClass.getName() + "." + method.getName() + " is not static"); + } + method.setAccessible(true); + method.invoke(null); + } catch (NoSuchMethodException e) { + RobolectricInternals.performStaticInitialization(clazz); + } catch (InvocationTargetException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } else { + RobolectricInternals.performStaticInitialization(clazz); + } + } + + @Override + public Object initializing(Object instance) { + return createShadowFor(instance); + } + + @Override + public Plan methodInvoked(String signature, boolean isStatic, Class<?> theClass) { + if (planCache.containsKey(signature)) { + return planCache.get(signature); + } + Plan plan = calculatePlan(signature, isStatic, theClass); + planCache.put(signature, plan); + return plan; + } + + @Override public MethodHandle findShadowMethod(Class<?> caller, String name, MethodType type, + boolean isStatic) throws IllegalAccessException { + ShadowConfig shadowConfig = shadowConfigs.get(caller); + if (shadowConfig == null) return CALL_REAL_CODE; + + ClassLoader classLoader = caller.getClassLoader(); + MethodType actualType = isStatic ? type : type.dropParameterTypes(0, 1); + Method method = findShadowMethod(classLoader, shadowConfig, name, actualType.parameterArray()); + if (method == null) { + return shadowConfig.callThroughByDefault ? CALL_REAL_CODE : DO_NOTHING; + } + + Class<?> declaredShadowedClass = getShadowedClass(method); + if (declaredShadowedClass.equals(Object.class)) { + // e.g. for equals(), hashCode(), toString() + return CALL_REAL_CODE; + } + + boolean shadowClassMismatch = !declaredShadowedClass.equals(caller); + if (shadowClassMismatch && !shadowConfig.inheritImplementationMethods) { + return CALL_REAL_CODE; + } else { + MethodHandle mh = LOOKUP.unreflect(method); + + // Robolectric doesn't actually look for static, this for example happens + // in MessageQueue.nativeInit() which used to be void non-static in 4.2. + if (!isStatic && Modifier.isStatic(method.getModifiers())) { + return dropArguments(mh, 0, Object.class); + } else { + return mh; + } + } + } + + private Plan calculatePlan(String signature, boolean isStatic, Class<?> theClass) { + final InvocationProfile invocationProfile = new InvocationProfile(signature, isStatic, theClass.getClassLoader()); + ShadowConfig shadowConfig = getShadowConfig(theClass); + + if (shadowConfig == null || !shadowConfig.supportsSdk(apiLevel)) { + return CALL_REAL_CODE_PLAN; + } else { + try { + final ClassLoader classLoader = theClass.getClassLoader(); + Class<?>[] types = invocationProfile.getParamClasses(classLoader); + Method shadowMethod = findShadowMethod(classLoader, shadowConfig, invocationProfile.methodName, types); + if (shadowMethod == null) { + return shadowConfig.callThroughByDefault + ? CALL_REAL_CODE_PLAN + : strict(invocationProfile) ? CALL_REAL_CODE_PLAN : DO_NOTHING_PLAN; + } + + final Class<?> declaredShadowedClass = getShadowedClass(shadowMethod); + + if (declaredShadowedClass.equals(Object.class)) { + // e.g. for equals(), hashCode(), toString() + return CALL_REAL_CODE_PLAN; + } + + boolean shadowClassMismatch = !declaredShadowedClass.equals(invocationProfile.clazz); + if (shadowClassMismatch && (!shadowConfig.inheritImplementationMethods || strict(invocationProfile))) { + return CALL_REAL_CODE_PLAN; + } else { + return new ShadowMethodPlan(shadowMethod); + } + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + } + + private Method findShadowMethod(ClassLoader classLoader, ShadowConfig config, String name, Class<?>[] types) { + try { + Class<?> shadowClass = Class.forName(config.shadowClassName, false, classLoader); + Method method = findShadowMethodInternal(shadowClass, name, types); + if (method == null && config.looseSignatures) { + Class<?>[] genericTypes = MethodType.genericMethodType(types.length).parameterArray(); + method = findShadowMethodInternal(shadowClass, name, genericTypes); + } + + return method; + } catch (ClassNotFoundException e) { + throw new IllegalStateException(e); + } + } + + private ShadowConfig getShadowConfig(Class clazz) { + ShadowConfig shadowConfig = shadowConfigCache.get(clazz); + if (shadowConfig == null) { + shadowConfig = shadowMap.get(clazz); + shadowConfigCache.put(clazz, shadowConfig == null ? NO_SHADOW_CONFIG : shadowConfig); + return shadowConfig; + } else { + return (shadowConfig == NO_SHADOW_CONFIG) ? null : shadowConfig; + } + } + + private boolean isAndroidSupport(InvocationProfile invocationProfile) { + return invocationProfile.clazz.getName().startsWith("android.support"); + } + + private boolean strict(InvocationProfile invocationProfile) { + return isAndroidSupport(invocationProfile) || invocationProfile.isDeclaredOnObject(); + } + + private Method findShadowMethodInternal(Class<?> shadowClass, String methodName, Class<?>[] paramClasses) throws ClassNotFoundException { + try { + Method method = shadowClass.getMethod(methodName, paramClasses); + Implementation implementation = getImplementationAnnotation(method); + return matchesSdk(implementation) ? method : null; + + // todo: allow per-version overloading +// if (method == null) { +// String methodPrefix = name + "$$"; +// for (Method candidateMethod : shadowClass.getMethods()) { +// if (candidateMethod.getName().startsWith(methodPrefix)) { +// +// } +// } +// } + + } catch (NoSuchMethodException e) { + return null; + } + } + + private boolean matchesSdk(Implementation implementation) { + return implementation.minSdk() <= apiLevel && (implementation.maxSdk() == -1 || implementation.maxSdk() >= apiLevel); + } + + private Class<?> getShadowedClass(Method shadowMethod) { + Class<?> shadowingClass = shadowMethod.getDeclaringClass(); + if (shadowingClass.equals(Object.class)) { + return Object.class; + } + + Implements implementsAnnotation = shadowingClass.getAnnotation(Implements.class); + if (implementsAnnotation == null) { + throw new RuntimeException(shadowingClass + " has no @" + Implements.class.getSimpleName() + " annotation"); + } + String shadowedClassName = implementsAnnotation.className(); + if (shadowedClassName.isEmpty()) { + return implementsAnnotation.value(); + } else { + try { + return shadowingClass.getClassLoader().loadClass(shadowedClassName); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + } + + private Implementation getImplementationAnnotation(Method method) { + if (method == null) { + return null; + } + Implementation implementation = method.getAnnotation(Implementation.class); + return implementation == null + ? ReflectionHelpers.defaultsFor(Implementation.class) + : implementation; + } + + @Override + public Object intercept(String signature, Object instance, Object[] params, Class theClass) throws Throwable { + final MethodSignature methodSignature = MethodSignature.parse(signature); + return interceptors.getInterceptionHandler(methodSignature).call(theClass, instance, params); + } + + @Override + public <T extends Throwable> T stripStackTrace(T throwable) { + if (STRIP_SHADOW_STACK_TRACES) { + List<StackTraceElement> stackTrace = new ArrayList<>(); + + String previousClassName = null; + String previousMethodName = null; + String previousFileName = null; + + for (StackTraceElement stackTraceElement : throwable.getStackTrace()) { + String methodName = stackTraceElement.getMethodName(); + String className = stackTraceElement.getClassName(); + String fileName = stackTraceElement.getFileName(); + + if (methodName.equals(previousMethodName) + && className.equals(previousClassName) + && fileName != null && fileName.equals(previousFileName) + && stackTraceElement.getLineNumber() < 0) { + continue; + } + + if (className.equals(ShadowMethodPlan.class.getName())) { + continue; + } + + if (methodName.startsWith(ShadowConstants.ROBO_PREFIX)) { + methodName = methodName.substring(ShadowConstants.ROBO_PREFIX.length()); + stackTraceElement = new StackTraceElement(className, methodName, + stackTraceElement.getFileName(), stackTraceElement.getLineNumber()); + } + + if (className.startsWith("sun.reflect.") || className.startsWith("java.lang.reflect.")) { + continue; + } + + stackTrace.add(stackTraceElement); + + previousClassName = className; + previousMethodName = methodName; + previousFileName = fileName; + } + throwable.setStackTrace(stackTrace.toArray(new StackTraceElement[stackTrace.size()])); + } + return throwable; + } + + public Object createShadowFor(Object instance) { + String shadowClassName = getShadowClassName(instance.getClass()); + + if (shadowClassName == null) return NO_SHADOW; + + try { + Class<?> shadowClass = loadClass(shadowClassName, instance.getClass().getClassLoader()); + Object shadow = shadowClass.newInstance(); + injectRealObjectOn(shadow, shadowClass, instance); + + return shadow; + } catch (InstantiationException | IllegalAccessException e) { + throw new RuntimeException("Could not instantiate shadow, missing public empty constructor.", e); + } + } + + @Override public MethodHandle getShadowCreator(Class<?> caller) { + String shadowClassName = getShadowClassNameInvoke(caller); + + if (shadowClassName == null) return dropArguments(NO_SHADOW_HANDLE, 0, caller); + + try { + Class<?> shadowClass = Class.forName(shadowClassName, false, caller.getClassLoader()); + MethodHandle constructor = LOOKUP.findConstructor(shadowClass, methodType(void.class)); + MetaShadow metaShadow = getMetaShadow(shadowClass); + + MethodHandle mh = identity(shadowClass); // (instance) + mh = dropArguments(mh, 1, caller); // (instance) + for (Field field : metaShadow.realObjectFields) { + MethodHandle setter = LOOKUP.unreflectSetter(field); + MethodType setterType = mh.type().changeReturnType(void.class); + mh = foldArguments(mh, setter.asType(setterType)); + } + mh = foldArguments(mh, constructor); // (shadow, instance) + + return mh; // (instance) + } catch (NoSuchMethodException | IllegalAccessException e) { + throw new RuntimeException("Could not instantiate shadow, missing public empty constructor.", e); + } catch (ClassNotFoundException e) { + throw new RuntimeException("Could not instantiate shadow", e); + } + } + + private String getShadowClassNameInvoke(Class<?> cl) { + Class clazz = cl; + ShadowConfig shadowConfig = null; + while (shadowConfig == null && clazz != null) { + shadowConfig = shadowConfigs.get(clazz); + clazz = clazz.getSuperclass(); + } + return shadowConfig == null ? null : shadowConfig.shadowClassName; + } + + private String getShadowClassName(Class<?> cl) { + Class clazz = cl; + ShadowConfig shadowConfig = null; + while ((shadowConfig == null || !shadowConfig.supportsSdk(apiLevel)) && clazz != null) { + shadowConfig = getShadowConfig(clazz); + clazz = clazz.getSuperclass(); + } + return shadowConfig == null ? null : shadowConfig.shadowClassName; + } + + private void injectRealObjectOn(Object shadow, Class<?> shadowClass, Object instance) { + MetaShadow metaShadow = getMetaShadow(shadowClass); + for (Field realObjectField : metaShadow.realObjectFields) { + writeField(shadow, instance, realObjectField); + } + } + + private MetaShadow getMetaShadow(Class<?> shadowClass) { + synchronized (metaShadowMap) { + MetaShadow metaShadow = metaShadowMap.get(shadowClass); + if (metaShadow == null) { + metaShadow = new MetaShadow(shadowClass); + metaShadowMap.put(shadowClass, metaShadow); + } + return metaShadow; + } + } + + private Class<?> findDirectShadowClass(Class<?> originalClass) { + ShadowConfig shadowConfig = getShadowConfig(originalClass); + if (shadowConfig == null || !shadowConfig.supportsSdk(apiLevel)) { + return null; + } + return loadClass(shadowConfig.shadowClassName, originalClass.getClassLoader()); + } + + private static void writeField(Object target, Object value, Field realObjectField) { + try { + realObjectField.set(target, value); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + private static class ShadowMethodPlan implements Plan { + private final Method shadowMethod; + + public ShadowMethodPlan(Method shadowMethod) { + this.shadowMethod = shadowMethod; + } + + @Override + public Object run(Object instance, Object roboData, Object[] params) throws Throwable { + //noinspection UnnecessaryLocalVariable + Object shadow = roboData; + try { + return shadowMethod.invoke(shadow, params); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("attempted to invoke " + shadowMethod + + (shadow == null ? "" : " on instance of " + shadow.getClass() + ", but " + shadow.getClass().getSimpleName() + " doesn't extend " + shadowMethod.getDeclaringClass().getSimpleName())); + } catch (InvocationTargetException e) { + throw e.getCause(); + } + } + + @Override + public String describe() { + return shadowMethod.toString(); + } + } + + private class MetaShadow { + final List<Field> realObjectFields = new ArrayList<>(); + + public MetaShadow(Class<?> shadowClass) { + while (shadowClass != null) { + for (Field field : shadowClass.getDeclaredFields()) { + if (field.isAnnotationPresent(RealObject.class)) { + if (Modifier.isStatic(field.getModifiers())) { + String message = "@RealObject must be on a non-static field, " + shadowClass; + System.err.println(message); + throw new IllegalArgumentException(message); + } + field.setAccessible(true); + realObjectFields.add(field); + } + } + shadowClass = shadowClass.getSuperclass(); + } + } + } +} diff --git a/robolectric-sandbox/src/main/java/org/robolectric/util/Function.java b/robolectric-sandbox/src/main/java/org/robolectric/util/Function.java new file mode 100644 index 000000000..49ccad1f0 --- /dev/null +++ b/robolectric-sandbox/src/main/java/org/robolectric/util/Function.java @@ -0,0 +1,8 @@ +package org.robolectric.util; + +/** + * Interface defining a function object. + */ +public interface Function<R, T> { + R call(Class<?> theClass, T value, Object[] params); +} diff --git a/robolectric-sandbox/src/main/java/org/robolectric/util/JavaVersion.java b/robolectric-sandbox/src/main/java/org/robolectric/util/JavaVersion.java new file mode 100644 index 000000000..7f47b0acd --- /dev/null +++ b/robolectric-sandbox/src/main/java/org/robolectric/util/JavaVersion.java @@ -0,0 +1,31 @@ +package org.robolectric.util; + +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; + +public class JavaVersion implements Comparable<JavaVersion> { + private final List<Integer> versions; + + public JavaVersion(String version) { + versions = new ArrayList<>(); + Scanner s = new Scanner(version).useDelimiter("[^\\d]+"); + while (s.hasNext()) { + versions.add(s.nextInt()); + } + } + + @Override public int compareTo(JavaVersion o) { + List<Integer> versions2 = o.versions; + int max = Math.min(versions.size(), versions2.size()); + for (int i = 0; i < max; i++) { + int compare = versions.get(i).compareTo(versions2.get(i)); + if (compare != 0) { + return compare; + } + } + + // Assume longer is newer + return Integer.compare(versions.size(), versions2.size()); + } +} diff --git a/robolectric-sandbox/src/test/java/org/robolectric/ClassicSuperHandlingTest.java b/robolectric-sandbox/src/test/java/org/robolectric/ClassicSuperHandlingTest.java new file mode 100644 index 000000000..069896f96 --- /dev/null +++ b/robolectric-sandbox/src/test/java/org/robolectric/ClassicSuperHandlingTest.java @@ -0,0 +1,83 @@ +package org.robolectric; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.annotation.Implements; +import org.robolectric.annotation.RealObject; +import org.robolectric.annotation.internal.Instrument; +import org.robolectric.internal.InstrumentingTestRunner; +import org.robolectric.internal.bytecode.RoboConfig; + +import static org.junit.Assert.assertEquals; + +@RunWith(InstrumentingTestRunner.class) +public class ClassicSuperHandlingTest { + @Test + @RoboConfig(shadows = {ChildShadow.class, ParentShadow.class, GrandparentShadow.class}) + public void uninstrumentedSubclassesShouldBeAbleToCallSuperWithoutLooping() throws Exception { + assertEquals("4-3s-2s-1s-boof", new BabiesHavingBabies().method("boof")); + } + + @Test + @RoboConfig(shadows = {ChildShadow.class, ParentShadow.class, GrandparentShadow.class}) + public void shadowInvocationWhenAllAreShadowed() throws Exception { + assertEquals("3s-2s-1s-boof", new Child().method("boof")); + assertEquals("2s-1s-boof", new Parent().method("boof")); + assertEquals("1s-boof", new Grandparent().method("boof")); + } + + @Implements(Child.class) + public static class ChildShadow extends ParentShadow { + private @RealObject Child realObject; + + @Override public String method(String value) { + return "3s-" + super.method(value); + } + } + + @Implements(Parent.class) + public static class ParentShadow extends GrandparentShadow { + private @RealObject Parent realObject; + + @Override public String method(String value) { + return "2s-" + super.method(value); + } + } + + @Implements(Grandparent.class) + public static class GrandparentShadow { + private @RealObject Grandparent realObject; + + public String method(String value) { + return "1s-" + value; + } + } + + private static class BabiesHavingBabies extends Child { + @Override + public String method(String value) { + return "4-" + super.method(value); + } + } + + @Instrument + public static class Child extends Parent { + @Override public String method(String value) { + throw new RuntimeException("Stub!"); + } + } + + @Instrument + public static class Parent extends Grandparent { + @Override public String method(String value) { + throw new RuntimeException("Stub!"); + } + } + + @Instrument + private static class Grandparent { + public String method(String value) { + throw new RuntimeException("Stub!"); + } + } +} diff --git a/robolectric-sandbox/src/test/java/org/robolectric/InstrumentingClassLoaderTest.java b/robolectric-sandbox/src/test/java/org/robolectric/InstrumentingClassLoaderTest.java new file mode 100644 index 000000000..2b389e046 --- /dev/null +++ b/robolectric-sandbox/src/test/java/org/robolectric/InstrumentingClassLoaderTest.java @@ -0,0 +1,815 @@ +package org.robolectric; + +import org.jetbrains.annotations.NotNull; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; +import org.robolectric.internal.InvokeDynamic; +import org.robolectric.internal.Shadow; +import org.robolectric.internal.ShadowConstants; +import org.robolectric.internal.ShadowExtractor; +import org.robolectric.internal.ShadowImpl; +import org.robolectric.internal.ShadowedObject; +import org.robolectric.internal.bytecode.ClassHandler; +import org.robolectric.internal.bytecode.ClassInfo; +import org.robolectric.internal.bytecode.InstrumentationConfiguration; +import org.robolectric.internal.bytecode.InstrumentingClassLoader; +import org.robolectric.internal.bytecode.Interceptor; +import org.robolectric.internal.bytecode.Interceptors; +import org.robolectric.internal.bytecode.InvocationProfile; +import org.robolectric.internal.bytecode.InvokeDynamicSupport; +import org.robolectric.internal.bytecode.MethodRef; +import org.robolectric.internal.bytecode.RobolectricInternals; +import org.robolectric.internal.bytecode.ShadowInvalidator; +import org.robolectric.testing.AChild; +import org.robolectric.testing.AClassThatCallsAMethodReturningAForgettableClass; +import org.robolectric.testing.AClassThatExtendsAClassWithFinalEqualsHashCode; +import org.robolectric.testing.AClassThatRefersToAForgettableClass; +import org.robolectric.testing.AClassThatRefersToAForgettableClassInItsConstructor; +import org.robolectric.testing.AClassThatRefersToAForgettableClassInMethodCalls; +import org.robolectric.testing.AClassThatRefersToAForgettableClassInMethodCallsReturningPrimitive; +import org.robolectric.testing.AClassToForget; +import org.robolectric.testing.AClassToRemember; +import org.robolectric.testing.AClassWithEqualsHashCodeToString; +import org.robolectric.testing.AClassWithFunnyConstructors; +import org.robolectric.testing.AClassWithMethodReturningArray; +import org.robolectric.testing.AClassWithMethodReturningBoolean; +import org.robolectric.testing.AClassWithMethodReturningDouble; +import org.robolectric.testing.AClassWithMethodReturningInteger; +import org.robolectric.testing.AClassWithNativeMethod; +import org.robolectric.testing.AClassWithNativeMethodReturningPrimitive; +import org.robolectric.testing.AClassWithNoDefaultConstructor; +import org.robolectric.testing.AClassWithStaticMethod; +import org.robolectric.testing.AClassWithoutEqualsHashCodeToString; +import org.robolectric.testing.AFinalClass; +import org.robolectric.testing.AnEnum; +import org.robolectric.testing.AnExampleClass; +import org.robolectric.testing.AnInstrumentedChild; +import org.robolectric.testing.AnInstrumentedClassWithoutToStringWithSuperToString; +import org.robolectric.testing.AnUninstrumentedClass; +import org.robolectric.testing.AnUninstrumentedParent; +import org.robolectric.util.ReflectionHelpers; +import org.robolectric.util.Util; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.invoke.SwitchPoint; +import java.lang.reflect.Array; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static java.lang.invoke.MethodHandles.*; +import static java.lang.invoke.MethodType.methodType; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.*; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.robolectric.util.ReflectionHelpers.newInstance; +import static org.robolectric.util.ReflectionHelpers.setStaticField; + +public class InstrumentingClassLoaderTest { + + private ClassLoader classLoader; + private List<String> transcript = new ArrayList<>(); + private MyClassHandler classHandler = new MyClassHandler(transcript); + private ShadowImpl shadow; + + @Before + public void setUp() throws Exception { + shadow = new ShadowImpl(); + } + + @Test + public void shouldMakeClassesNonFinal() throws Exception { + Class<?> clazz = loadClass(AFinalClass.class); + assertEquals(0, clazz.getModifiers() & Modifier.FINAL); + } + + @Test + public void forClassesWithNoDefaultConstructor_shouldCreateOneButItShouldNotCallShadow() throws Exception { + Constructor<?> defaultCtor = loadClass(AClassWithNoDefaultConstructor.class).getConstructor(); + assertTrue(Modifier.isPublic(defaultCtor.getModifiers())); + defaultCtor.setAccessible(true); + Object instance = defaultCtor.newInstance(); + assertThat(ShadowExtractor.extract(instance)).isNotNull(); + assertThat(transcript).isEmpty(); + } + + @Test + public void shouldDelegateToHandlerForConstructors() throws Exception { + Class<?> clazz = loadClass(AClassWithNoDefaultConstructor.class); + Constructor<?> ctor = clazz.getDeclaredConstructor(String.class); + assertTrue(Modifier.isPublic(ctor.getModifiers())); + ctor.setAccessible(true); + Object instance = ctor.newInstance("new one"); + assertThat(transcript).containsExactly( + "methodInvoked: AClassWithNoDefaultConstructor.__constructor__(java.lang.String new one)"); + + Field nameField = clazz.getDeclaredField("name"); + nameField.setAccessible(true); + assertNull(nameField.get(instance)); + } + + @Test + public void shouldDelegateClassLoadForUnacquiredClasses() throws Exception { + InstrumentationConfiguration config = mock(InstrumentationConfiguration.class); + when(config.shouldAcquire(anyString())).thenReturn(false); + when(config.shouldInstrument(any(ClassInfo.class))).thenReturn(false); + ClassLoader classLoader = new InstrumentingClassLoader(config); + Class<?> exampleClass = classLoader.loadClass(AnExampleClass.class.getName()); + assertSame(getClass().getClassLoader(), exampleClass.getClassLoader()); + } + + @Test + public void shouldPerformClassLoadForAcquiredClasses() throws Exception { + ClassLoader classLoader = new InstrumentingClassLoader(configureBuilder().build()); + Class<?> exampleClass = classLoader.loadClass(AnUninstrumentedClass.class.getName()); + assertSame(classLoader, exampleClass.getClassLoader()); + try { + exampleClass.getField(ShadowConstants.CLASS_HANDLER_DATA_FIELD_NAME); + fail("class shouldn't be instrumented!"); + } catch (Exception e) { + // expected + } + } + + @Test + public void shouldPerformClassLoadAndInstrumentLoadForInstrumentedClasses() throws Exception { + ClassLoader classLoader = new InstrumentingClassLoader(configureBuilder().build()); + Class<?> exampleClass = classLoader.loadClass(AnExampleClass.class.getName()); + assertSame(classLoader, exampleClass.getClassLoader()); + Field roboDataField = exampleClass.getField(ShadowConstants.CLASS_HANDLER_DATA_FIELD_NAME); + assertNotNull(roboDataField); + assertThat(Modifier.isPublic(roboDataField.getModifiers())).isTrue(); + + // field should be marked final so Mockito doesn't try to @InjectMocks on it; + // see https://github.com/robolectric/robolectric/issues/2442 + assertThat(Modifier.isFinal(roboDataField.getModifiers())).isTrue(); + } + + @Test + public void callingNormalMethodShouldInvokeClassHandler() throws Exception { + Class<?> exampleClass = loadClass(AnExampleClass.class); + Method normalMethod = exampleClass.getMethod("normalMethod", String.class, int.class); + + Object exampleInstance = exampleClass.newInstance(); + assertEquals("response from methodInvoked: AnExampleClass.normalMethod(java.lang.String value1, int 123)", + normalMethod.invoke(exampleInstance, "value1", 123)); + assertThat(transcript).containsExactly( + "methodInvoked: AnExampleClass.__constructor__()", + "methodInvoked: AnExampleClass.normalMethod(java.lang.String value1, int 123)"); + } + + @Test + public void shouldGenerateClassSpecificDirectAccessMethod() throws Exception { + Class<?> exampleClass = loadClass(AnExampleClass.class); + String methodName = shadow.directMethodName("normalMethod"); + Method directMethod = exampleClass.getDeclaredMethod(methodName, String.class, int.class); + directMethod.setAccessible(true); + Object exampleInstance = exampleClass.newInstance(); + assertEquals("normalMethod(value1, 123)", directMethod.invoke(exampleInstance, "value1", 123)); + assertThat(transcript).containsExactly( + "methodInvoked: AnExampleClass.__constructor__()"); + } + + @Test + public void soMockitoDoesntExplodeDueToTooManyMethods_shouldGenerateClassSpecificDirectAccessMethodWhichIsPrivateAndFinal() throws Exception { + Class<?> exampleClass = loadClass(AnExampleClass.class); + String methodName = shadow.directMethodName("normalMethod"); + Method directMethod = exampleClass.getDeclaredMethod(methodName, String.class, int.class); + assertTrue(Modifier.isPrivate(directMethod.getModifiers())); + assertTrue(Modifier.isFinal(directMethod.getModifiers())); + } + + @Test + public void callingStaticMethodShouldInvokeClassHandler() throws Exception { + Class<?> exampleClass = loadClass(AClassWithStaticMethod.class); + Method normalMethod = exampleClass.getMethod("staticMethod", String.class); + + assertEquals( + "response from methodInvoked: AClassWithStaticMethod.staticMethod(java.lang.String value1)", + normalMethod.invoke(null, "value1")); + assertThat(transcript).containsExactly( + "methodInvoked: AClassWithStaticMethod.staticMethod(java.lang.String value1)"); + } + + @Test + public void callingStaticDirectAccessMethodShouldWork() throws Exception { + Class<?> exampleClass = loadClass(AClassWithStaticMethod.class); + String methodName = shadow.directMethodName("staticMethod"); + Method directMethod = exampleClass.getDeclaredMethod(methodName, String.class); + directMethod.setAccessible(true); + assertEquals("staticMethod(value1)", directMethod.invoke(null, "value1")); + } + + @Test + public void callingNormalMethodReturningIntegerShouldInvokeClassHandler() throws Exception { + Class<?> exampleClass = loadClass(AClassWithMethodReturningInteger.class); + classHandler.valueToReturn = 456; + + Method normalMethod = exampleClass.getMethod("normalMethodReturningInteger", int.class); + Object exampleInstance = exampleClass.newInstance(); + assertEquals(456, normalMethod.invoke(exampleInstance, 123)); + assertThat(transcript).containsExactly( + "methodInvoked: AClassWithMethodReturningInteger.__constructor__()", + "methodInvoked: AClassWithMethodReturningInteger.normalMethodReturningInteger(int 123)"); + } + + @Test + public void callingMethodReturningDoubleShouldInvokeClassHandler() throws Exception { + Class<?> exampleClass = loadClass(AClassWithMethodReturningDouble.class); + classHandler.valueToReturn = 456; + + Method normalMethod = exampleClass.getMethod("normalMethodReturningDouble", double.class); + Object exampleInstance = exampleClass.newInstance(); + assertEquals(456.0, normalMethod.invoke(exampleInstance, 123d)); + assertThat(transcript).containsExactly( + "methodInvoked: AClassWithMethodReturningDouble.__constructor__()", + "methodInvoked: AClassWithMethodReturningDouble.normalMethodReturningDouble(double 123.0)"); + } + + @Test + public void callingNativeMethodShouldInvokeClassHandler() throws Exception { + Class<?> exampleClass = loadClass(AClassWithNativeMethod.class); + Method normalMethod = exampleClass.getDeclaredMethod("nativeMethod", String.class, int.class); + Object exampleInstance = exampleClass.newInstance(); + assertEquals("response from methodInvoked: AClassWithNativeMethod.nativeMethod(java.lang.String value1, int 123)", + normalMethod.invoke(exampleInstance, "value1", 123)); + assertThat(transcript).containsExactly( + "methodInvoked: AClassWithNativeMethod.__constructor__()", + "methodInvoked: AClassWithNativeMethod.nativeMethod(java.lang.String value1, int 123)"); + } + + @Test + public void directlyCallingNativeMethodShouldBeNoOp() throws Exception { + Class<?> exampleClass = loadClass(AClassWithNativeMethod.class); + Object exampleInstance = exampleClass.newInstance(); + Method directMethod = findDirectMethod(exampleClass, "nativeMethod", String.class, int.class); + assertThat(Modifier.isNative(directMethod.getModifiers())).isFalse(); + + assertThat(directMethod.invoke(exampleInstance, "", 1)).isNull(); + } + + @Test + public void directlyCallingNativeMethodReturningPrimitiveShouldBeNoOp() throws Exception { + Class<?> exampleClass = loadClass(AClassWithNativeMethodReturningPrimitive.class); + Object exampleInstance = exampleClass.newInstance(); + Method directMethod = findDirectMethod(exampleClass, "nativeMethod"); + assertThat(Modifier.isNative(directMethod.getModifiers())).isFalse(); + + assertThat(directMethod.invoke(exampleInstance)).isEqualTo(0); + } + + @Test + public void shouldHandleMethodsReturningBoolean() throws Exception { + Class<?> exampleClass = loadClass(AClassWithMethodReturningBoolean.class); + classHandler.valueToReturn = true; + + Method directMethod = exampleClass.getMethod("normalMethodReturningBoolean", boolean.class, boolean[].class); + directMethod.setAccessible(true); + Object exampleInstance = exampleClass.newInstance(); + assertEquals(true, directMethod.invoke(exampleInstance, true, new boolean[0])); + assertThat(transcript).containsExactly( + "methodInvoked: AClassWithMethodReturningBoolean.__constructor__()", + "methodInvoked: AClassWithMethodReturningBoolean.normalMethodReturningBoolean(boolean true, boolean[] {})"); + } + + @Test + public void shouldHandleMethodsReturningArray() throws Exception { + Class<?> exampleClass = loadClass(AClassWithMethodReturningArray.class); + classHandler.valueToReturn = new String[]{"miao, mieuw"}; + + Method directMethod = exampleClass.getMethod("normalMethodReturningArray"); + directMethod.setAccessible(true); + Object exampleInstance = exampleClass.newInstance(); + assertThat(transcript).containsExactly( + "methodInvoked: AClassWithMethodReturningArray.__constructor__()"); + transcript.clear(); + assertArrayEquals(new String[]{"miao, mieuw"}, (String[]) directMethod.invoke(exampleInstance)); + assertThat(transcript).containsExactly( + "methodInvoked: AClassWithMethodReturningArray.normalMethodReturningArray()"); + } + + @Test + public void shouldInvokeShadowForEachConstructorInInheritanceTree() throws Exception { + loadClass(AChild.class).newInstance(); + assertThat(transcript).containsExactly( + "methodInvoked: AGrandparent.__constructor__()", + "methodInvoked: AParent.__constructor__()", + "methodInvoked: AChild.__constructor__()"); + } + + @Test + public void shouldRetainSuperCallInConstructor() throws Exception { + Class<?> aClass = loadClass(AnInstrumentedChild.class); + Object o = aClass.getDeclaredConstructor(String.class).newInstance("hortense"); + assertEquals("HORTENSE's child", aClass.getSuperclass().getDeclaredField("parentName").get(o)); + assertNull(aClass.getDeclaredField("childName").get(o)); + } + + @Test + public void shouldCorrectlySplitStaticPrepFromConstructorChaining() throws Exception { + Class<?> aClass = loadClass(AClassWithFunnyConstructors.class); + Object o = aClass.getDeclaredConstructor(String.class).newInstance("hortense"); + assertThat(transcript).containsExactly( + "methodInvoked: AClassWithFunnyConstructors.__constructor__(" + AnUninstrumentedParent.class.getName() + " UninstrumentedParent{parentName='hortense'}, java.lang.String foo)", + "methodInvoked: AClassWithFunnyConstructors.__constructor__(java.lang.String hortense)"); + + // should not run constructor bodies... + assertEquals(null, getDeclaredFieldValue(aClass, o, "name")); + assertEquals(null, getDeclaredFieldValue(aClass, o, "uninstrumentedParent")); + } + + @Test + public void shouldGenerateClassSpecificDirectAccessMethodForConstructorWhichDoesNotCallSuper() throws Exception { + Class<?> aClass = loadClass(AClassWithFunnyConstructors.class); + Object instance = aClass.getConstructor(String.class).newInstance("horace"); + assertThat(transcript).containsExactly( + "methodInvoked: AClassWithFunnyConstructors.__constructor__(" + AnUninstrumentedParent.class.getName() + " UninstrumentedParent{parentName='horace'}, java.lang.String foo)", + "methodInvoked: AClassWithFunnyConstructors.__constructor__(java.lang.String horace)"); + transcript.clear(); + + // each directly-accessible constructor body will need to be called explicitly, with the correct args... + + Class<?> uninstrumentedParentClass = loadClass(AnUninstrumentedParent.class); + Method directMethod = findDirectMethod(aClass, "__constructor__", uninstrumentedParentClass, String.class); + Object uninstrumentedParentIn = uninstrumentedParentClass.getDeclaredConstructor(String.class).newInstance("hortense"); + assertEquals(null, directMethod.invoke(instance, uninstrumentedParentIn, "foo")); + assertThat(transcript).isEmpty(); + + assertEquals(null, getDeclaredFieldValue(aClass, instance, "name")); + Object uninstrumentedParentOut = getDeclaredFieldValue(aClass, instance, "uninstrumentedParent"); + assertEquals("hortense", getDeclaredFieldValue(uninstrumentedParentClass, uninstrumentedParentOut, "parentName")); + + Method directMethod2 = findDirectMethod(aClass, "__constructor__", String.class); + assertEquals(null, directMethod2.invoke(instance, "hortense")); + assertThat(transcript).isEmpty(); + + assertEquals("hortense", getDeclaredFieldValue(aClass, instance, "name")); + } + + private Method findDirectMethod(Class<?> declaringClass, String methodName, Class<?>... argClasses) throws NoSuchMethodException { + String directMethodName = shadow.directMethodName(methodName); + Method directMethod = declaringClass.getDeclaredMethod(directMethodName, argClasses); + directMethod.setAccessible(true); + return directMethod; + } + + @Test + public void shouldNotInstrumentFinalEqualsHashcode() throws ClassNotFoundException { + Class<?> theClass = loadClass(AClassThatExtendsAClassWithFinalEqualsHashCode.class); + } + + @Test + public void shouldInstrumentEqualsAndHashCodeAndToStringEvenWhenUndeclared() throws Exception { + Class<?> theClass = loadClass(AClassWithoutEqualsHashCodeToString.class); + Object instance = theClass.newInstance(); + assertThat(transcript).containsExactly("methodInvoked: AClassWithoutEqualsHashCodeToString.__constructor__()"); + transcript.clear(); + + instance.toString(); + assertThat(transcript).containsExactly("methodInvoked: AClassWithoutEqualsHashCodeToString.toString()"); + transcript.clear(); + + classHandler.valueToReturn = true; + //noinspection ResultOfMethodCallIgnored,ObjectEqualsNull + instance.equals(null); + assertThat(transcript).containsExactly("methodInvoked: AClassWithoutEqualsHashCodeToString.equals(java.lang.Object null)"); + transcript.clear(); + + classHandler.valueToReturn = 42; + //noinspection ResultOfMethodCallIgnored + instance.hashCode(); + assertThat(transcript).containsExactly("methodInvoked: AClassWithoutEqualsHashCodeToString.hashCode()"); + } + + @Test + public void shouldAlsoInstrumentEqualsAndHashCodeAndToStringWhenDeclared() throws Exception { + Class<?> theClass = loadClass(AClassWithEqualsHashCodeToString.class); + Object instance = theClass.newInstance(); + assertThat(transcript).containsExactly("methodInvoked: AClassWithEqualsHashCodeToString.__constructor__()"); + transcript.clear(); + + instance.toString(); + assertThat(transcript).containsExactly("methodInvoked: AClassWithEqualsHashCodeToString.toString()"); + transcript.clear(); + + classHandler.valueToReturn = true; + //noinspection ResultOfMethodCallIgnored,ObjectEqualsNull + instance.equals(null); + assertThat(transcript).containsExactly("methodInvoked: AClassWithEqualsHashCodeToString.equals(java.lang.Object null)"); + transcript.clear(); + + classHandler.valueToReturn = 42; + //noinspection ResultOfMethodCallIgnored + instance.hashCode(); + assertThat(transcript).containsExactly("methodInvoked: AClassWithEqualsHashCodeToString.hashCode()"); + } + + @Test + public void shouldProperlyCallSuperWhenForcingDeclarationOfEqualsHashCodeToString() throws Exception { + Class<?> theClass = loadClass(AnInstrumentedClassWithoutToStringWithSuperToString.class); + Object instance = theClass.newInstance(); + assertThat(transcript).containsExactly("methodInvoked: AnInstrumentedClassWithoutToStringWithSuperToString.__constructor__()"); + transcript.clear(); + + instance.toString(); + assertThat(transcript).containsExactly("methodInvoked: AnInstrumentedClassWithoutToStringWithSuperToString.toString()"); + + assertEquals("baaaaaah", findDirectMethod(theClass, "toString").invoke(instance)); + } + + @Test + public void shouldRemapClasses() throws Exception { + setClassLoader(new InstrumentingClassLoader(createRemappingConfig())); + Class<?> theClass = loadClass(AClassThatRefersToAForgettableClass.class); + assertEquals(loadClass(AClassToRemember.class), theClass.getField("someField").getType()); + assertEquals(Array.newInstance(loadClass(AClassToRemember.class), 0).getClass(), theClass.getField("someFields").getType()); + } + + private InstrumentationConfiguration createRemappingConfig() { + return configureBuilder() + .addClassNameTranslation(AClassToForget.class.getName(), AClassToRemember.class.getName()) + .build(); + } + + @Test + public void shouldFixTypesInFieldAccess() throws Exception { + setClassLoader(new InstrumentingClassLoader(createRemappingConfig())); + Class<?> theClass = loadClass(AClassThatRefersToAForgettableClassInItsConstructor.class); + Object instance = theClass.newInstance(); + Method method = theClass.getDeclaredMethod(shadow.directMethodName(ShadowConstants.CONSTRUCTOR_METHOD_NAME)); + method.setAccessible(true); + method.invoke(instance); + } + + @Test + public void shouldFixTypesInMethodArgsAndReturn() throws Exception { + setClassLoader(new InstrumentingClassLoader(createRemappingConfig())); + Class<?> theClass = loadClass(AClassThatRefersToAForgettableClassInMethodCalls.class); + assertNotNull(theClass.getDeclaredMethod("aMethod", int.class, loadClass(AClassToRemember.class), String.class)); + } + + @Test + public void shouldInterceptFilteredMethodInvocations() throws Exception { + setClassLoader(new InstrumentingClassLoader(configureBuilder() + .addInterceptedMethod(new MethodRef(AClassToForget.class, "forgettableMethod")) + .build())); + + Class<?> theClass = loadClass(AClassThatRefersToAForgettableClass.class); + Object instance = theClass.newInstance(); + Object output = theClass.getMethod("interactWithForgettableClass").invoke(shadow.directlyOn(instance, (Class<Object>) theClass)); + assertEquals("null, get this!", output); + } + + @Test + public void shouldInterceptFilteredStaticMethodInvocations() throws Exception { + setClassLoader(new InstrumentingClassLoader(configureBuilder() + .addInterceptedMethod(new MethodRef(AClassToForget.class, "forgettableStaticMethod")) + .build())); + + Class<?> theClass = loadClass(AClassThatRefersToAForgettableClass.class); + Object instance = theClass.newInstance(); + Object output = theClass.getMethod("interactWithForgettableStaticMethod").invoke(shadow.directlyOn(instance, (Class<Object>) theClass)); + assertEquals("yess? forget this: null", output); + } + + @Test + public void byte_shouldBeHandledAsReturnValueFromInterceptHandler() throws Exception { + if (InvokeDynamic.ENABLED) return; + classHandler.valueToReturnFromIntercept = (byte) 10; + assertThat(invokeInterceptedMethodOnAClassToForget("byteMethod")).isEqualTo((byte) 10); + } + + @Test + public void byteArray_shouldBeHandledAsReturnValueFromInterceptHandler() throws Exception { + if (InvokeDynamic.ENABLED) return; + classHandler.valueToReturnFromIntercept = new byte[]{10, 12, 14}; + assertThat(invokeInterceptedMethodOnAClassToForget("byteArrayMethod")).isEqualTo(new byte[]{10, 12, 14}); + } + + @Test + public void int_shouldBeHandledAsReturnValueFromInterceptHandler() throws Exception { + if (InvokeDynamic.ENABLED) return; + classHandler.valueToReturnFromIntercept = 20; + assertThat(invokeInterceptedMethodOnAClassToForget("intMethod")).isEqualTo(20); + } + + @Test + public void intArray_shouldBeHandledAsReturnValueFromInterceptHandler() throws Exception { + if (InvokeDynamic.ENABLED) return; + classHandler.valueToReturnFromIntercept = new int[]{20, 22, 24}; + assertThat(invokeInterceptedMethodOnAClassToForget("intArrayMethod")).isEqualTo(new int[]{20, 22, 24}); + } + + @Test + public void long_shouldBeHandledAsReturnValueFromInterceptHandler() throws Exception { + if (InvokeDynamic.ENABLED) return; + classHandler.valueToReturnFromIntercept = 30L; + assertThat(invokeInterceptedMethodOnAClassToForget("longMethod")).isEqualTo(30L); + } + + @Test + public void longArray_shouldBeHandledAsReturnValueFromInterceptHandler() throws Exception { + if (InvokeDynamic.ENABLED) return; + classHandler.valueToReturnFromIntercept = new long[] {30L, 32L, 34L}; + assertThat(invokeInterceptedMethodOnAClassToForget("longArrayMethod")).isEqualTo(new long[] {30L, 32L, 34L}); + } + + @Test + public void float_shouldBeHandledAsReturnValueFromInterceptHandler() throws Exception { + if (InvokeDynamic.ENABLED) return; + classHandler.valueToReturnFromIntercept = 40f; + assertThat(invokeInterceptedMethodOnAClassToForget("floatMethod")).isEqualTo(40f); + } + + @Test + public void floatArray_shouldBeHandledAsReturnValueFromInterceptHandler() throws Exception { + if (InvokeDynamic.ENABLED) return; + classHandler.valueToReturnFromIntercept = new float[] {50f, 52f, 54f}; + assertThat(invokeInterceptedMethodOnAClassToForget("floatArrayMethod")).isEqualTo(new float[] {50f, 52f, 54f}); + } + + @Test + public void double_shouldBeHandledAsReturnValueFromInterceptHandler() throws Exception { + if (InvokeDynamic.ENABLED) return; + classHandler.valueToReturnFromIntercept = 80.0; + assertThat(invokeInterceptedMethodOnAClassToForget("doubleMethod")).isEqualTo(80.0); + } + + @Test + public void doubleArray_shouldBeHandledAsReturnValueFromInterceptHandler() throws Exception { + if (InvokeDynamic.ENABLED) return; + classHandler.valueToReturnFromIntercept = new double[] {90.0, 92.0, 94.0}; + assertThat(invokeInterceptedMethodOnAClassToForget("doubleArrayMethod")).isEqualTo(new double[] {90.0, 92.0, 94.0}); + } + + @Test + public void short_shouldBeHandledAsReturnValueFromInterceptHandler() throws Exception { + if (InvokeDynamic.ENABLED) return; + classHandler.valueToReturnFromIntercept = (short) 60; + assertThat(invokeInterceptedMethodOnAClassToForget("shortMethod")).isEqualTo((short) 60); + } + + @Test + public void shortArray_shouldBeHandledAsReturnValueFromInterceptHandler() throws Exception { + if (InvokeDynamic.ENABLED) return; + classHandler.valueToReturnFromIntercept = new short[] {70, 72, 74}; + assertThat(invokeInterceptedMethodOnAClassToForget("shortArrayMethod")).isEqualTo(new short[] {70, 72, 74}); + } + + @Test + public void void_shouldBeHandledAsReturnValueFromInterceptHandler() throws Exception { + if (InvokeDynamic.ENABLED) return; + classHandler.valueToReturnFromIntercept = null; + assertThat(invokeInterceptedMethodOnAClassToForget("voidReturningMethod")).isNull(); + } + + private Object invokeInterceptedMethodOnAClassToForget(String methodName) throws Exception { + setClassLoader(new InstrumentingClassLoader(configureBuilder() + .addInterceptedMethod(new MethodRef(AClassToForget.class, "*")) + .build())); + Class<?> theClass = loadClass(AClassThatRefersToAForgettableClassInMethodCallsReturningPrimitive.class); + Object instance = theClass.newInstance(); + Method m = theClass.getDeclaredMethod(methodName); + m.setAccessible(true); + return m.invoke(shadow.directlyOn(instance, (Class<Object>) theClass)); + } + + @NotNull + private InstrumentationConfiguration.Builder configureBuilder() { + InstrumentationConfiguration.Builder builder = InstrumentationConfiguration.newBuilder(); + builder.doNotAcquirePackage("java.") + .doNotAcquirePackage("sun.") + .doNotAcquirePackage("com.sun.") + .doNotAcquireClass(ClassHandler.class) + .doNotAcquireClass(ClassHandler.Plan.class) + .doNotAcquireClass(Interceptors.class) + .doNotAcquireClass(ShadowedObject.class) + .doNotAcquireClass(ShadowInvalidator.class) + ; + return builder; + } + + @Test + public void shouldPassArgumentsFromInterceptedMethods() throws Exception { + if (InvokeDynamic.ENABLED) return; + classHandler.valueToReturnFromIntercept = 10L; + + setClassLoader(new InstrumentingClassLoader(configureBuilder() + .addInterceptedMethod(new MethodRef(AClassToForget.class, "*")) + .build())); + + Class<?> theClass = loadClass(AClassThatRefersToAForgettableClassInMethodCallsReturningPrimitive.class); + Object instance = theClass.newInstance(); + shadow.directlyOn(instance, (Class<Object>) theClass, "longMethod"); + assertThat(transcript).containsExactly( + "methodInvoked: AClassThatRefersToAForgettableClassInMethodCallsReturningPrimitive.__constructor__()", + "intercept: org/robolectric/internal/bytecode/testing/AClassToForget/longReturningMethod(Ljava/lang/String;IJ)J with params (str str, 123 123, 456 456)"); + } + + @Test + public void shouldRemapClassesWhileInterceptingMethods() throws Exception { + InstrumentationConfiguration config = configureBuilder() + .addClassNameTranslation(AClassToForget.class.getName(), AClassToRemember.class.getName()) + .addInterceptedMethod(new MethodRef(AClassThatCallsAMethodReturningAForgettableClass.class, "getAForgettableClass")) + .build(); + + setClassLoader(new InstrumentingClassLoader(config)); + Class<?> theClass = loadClass(AClassThatCallsAMethodReturningAForgettableClass.class); + theClass.getMethod("callSomeMethod").invoke(shadow.directlyOn(theClass.newInstance(), (Class<Object>) theClass)); + } + + @Test + public void shouldWorkWithEnums() throws Exception { + loadClass(AnEnum.class); + } + + @Test + public void shouldReverseAnArray() throws Exception { + assertArrayEquals(new Integer[]{5, 4, 3, 2, 1}, Util.reverse(new Integer[]{1, 2, 3, 4, 5})); + assertArrayEquals(new Integer[]{4, 3, 2, 1}, Util.reverse(new Integer[]{1, 2, 3, 4})); + assertArrayEquals(new Integer[]{1}, Util.reverse(new Integer[]{1})); + assertArrayEquals(new Integer[]{}, Util.reverse(new Integer[]{})); + } + + @Test public void shouldCacheMisses() throws Exception { + final List<String> transcript = new ArrayList<>(); + + InstrumentingClassLoader classLoader = new InstrumentingClassLoader(configureBuilder().build()) { + @Override + protected Class<?> findClass(String className) throws ClassNotFoundException { + transcript.add("find " + className); + throw new ClassNotFoundException(className); + } + }; + + try { + classLoader.loadClass("foo.AClass"); + } catch (ClassNotFoundException e) { + // expected + } + try { + classLoader.loadClass("foo.AClass"); + } catch (ClassNotFoundException e) { + // expected + } + + assertThat(transcript).containsExactly("find foo.AClass"); + } + + ///////////////////////////// + + private Object getDeclaredFieldValue(Class<?> aClass, Object o, String fieldName) throws Exception { + Field field = aClass.getDeclaredField(fieldName); + field.setAccessible(true); + return field.get(o); + } + + public static class MyClassHandler implements ClassHandler { + private static Object GENERATE_YOUR_OWN_VALUE = new Object(); + private List<String> transcript; + private Object valueToReturn = GENERATE_YOUR_OWN_VALUE; + private Object valueToReturnFromIntercept = null; + + public MyClassHandler(List<String> transcript) { + this.transcript = transcript; + } + + @Override + public void classInitializing(Class clazz) { + } + + @Override + public Object initializing(Object instance) { + return "a shadow!"; + } + + public Object methodInvoked(Class clazz, String methodName, Object instance, String[] paramTypes, Object[] params) { + StringBuilder buf = new StringBuilder(); + buf.append("methodInvoked: ").append(clazz.getSimpleName()).append(".").append(methodName).append("("); + for (int i = 0; i < paramTypes.length; i++) { + if (i > 0) buf.append(", "); + Object param = params[i]; + Object display = param == null ? "null" : param.getClass().isArray() ? "{}" : param; + buf.append(paramTypes[i]).append(" ").append(display); + } + buf.append(")"); + transcript.add(buf.toString()); + + if (valueToReturn != GENERATE_YOUR_OWN_VALUE) return valueToReturn; + return "response from " + buf.toString(); + } + + @Override + public Plan methodInvoked(String signature, boolean isStatic, Class<?> theClass) { + final InvocationProfile invocationProfile = new InvocationProfile(signature, isStatic, getClass().getClassLoader()); + return new Plan() { + @Override + public Object run(Object instance, Object roboData, Object[] params) throws Exception { + try { + return methodInvoked(invocationProfile.clazz, invocationProfile.methodName, instance, invocationProfile.paramTypes, params); + } catch (Throwable throwable) { + throw new RuntimeException(throwable); + } + } + + @Override + public String describe() { + return invocationProfile.methodName; + } + }; + } + + @Override public MethodHandle getShadowCreator(Class<?> caller) { + return dropArguments(constant(String.class, "a shadow!"), 0, caller); + } + + @SuppressWarnings("UnusedDeclaration") + private Object invoke(InvocationProfile invocationProfile, Object instance, Object[] params) { + return methodInvoked(invocationProfile.clazz, invocationProfile.methodName, instance, + invocationProfile.paramTypes, params); + } + + @Override public MethodHandle findShadowMethod(Class<?> theClass, String name, MethodType type, + boolean isStatic) throws IllegalAccessException { + String signature = getSignature(theClass, name, type, isStatic); + InvocationProfile invocationProfile = new InvocationProfile(signature, isStatic, getClass().getClassLoader()); + + try { + MethodHandle mh = MethodHandles.lookup().findVirtual(getClass(), "invoke", + methodType(Object.class, InvocationProfile.class, Object.class, Object[].class)); + mh = insertArguments(mh, 0, this, invocationProfile); + + if (isStatic) { + return mh.bindTo(null).asCollector(Object[].class, type.parameterCount()); + } else { + return mh.asCollector(Object[].class, type.parameterCount() - 1); + } + } catch (NoSuchMethodException e) { + throw new AssertionError(e); + } + } + + public String getSignature(Class<?> caller, String name, MethodType type, boolean isStatic) { + String className = caller.getName().replace('.', '/'); + // Remove implicit first argument + if (!isStatic) type = type.dropParameterTypes(0, 1); + return className + "/" + name + type.toMethodDescriptorString(); + } + + + @Override + public Object intercept(String signature, Object instance, Object[] params, Class theClass) throws Throwable { + StringBuilder buf = new StringBuilder(); + buf.append("intercept: ").append(signature).append(" with params ("); + for (int i = 0; i < params.length; i++) { + if (i > 0) buf.append(", "); + Object param = params[i]; + Object display = param == null ? "null" : param.getClass().isArray() ? "{}" : param; + buf.append(params[i]).append(" ").append(display); + } + buf.append(")"); + transcript.add(buf.toString()); + return valueToReturnFromIntercept; + } + + @Override + public <T extends Throwable> T stripStackTrace(T throwable) { + return throwable; + } + } + + private void setClassLoader(ClassLoader classLoader) { + this.classLoader = classLoader; + } + + private Class<?> loadClass(Class<?> clazz) throws ClassNotFoundException { + if (classLoader == null) { + classLoader = new InstrumentingClassLoader(configureBuilder().build()); + } + + setStaticField(classLoader.loadClass(InvokeDynamicSupport.class.getName()), "INTERCEPTORS", + new Interceptors(Collections.<Interceptor>emptyList())); + setStaticField(classLoader.loadClass(Shadow.class.getName()), "SHADOW_IMPL", + newInstance(classLoader.loadClass(ShadowImpl.class.getName()))); + + ShadowInvalidator invalidator = Mockito.mock(ShadowInvalidator.class); + when(invalidator.getSwitchPoint(any(Class.class))).thenReturn(new SwitchPoint()); + + String className = RobolectricInternals.class.getName(); + Class<?> robolectricInternalsClass = ReflectionHelpers.loadClass(classLoader, className); + ReflectionHelpers.setStaticField(robolectricInternalsClass, "classHandler", classHandler); + ReflectionHelpers.setStaticField(robolectricInternalsClass, "shadowInvalidator", invalidator); + + return classLoader.loadClass(clazz.getName()); + } +} diff --git a/robolectric-sandbox/src/test/java/org/robolectric/JavaVersionTest.java b/robolectric-sandbox/src/test/java/org/robolectric/JavaVersionTest.java new file mode 100644 index 000000000..818824d63 --- /dev/null +++ b/robolectric-sandbox/src/test/java/org/robolectric/JavaVersionTest.java @@ -0,0 +1,48 @@ +package org.robolectric; + +import org.junit.Test; +import org.robolectric.util.JavaVersion; + +import static org.assertj.core.api.Assertions.assertThat; + +public class JavaVersionTest { + @Test + public void jdk8() { + check("1.8.1u40", "1.8.5u60"); + check("1.8.0u40", "1.8.0u60"); + check("1.8.0u40", "1.8.0u100"); + } + + @Test + public void jdk9() { + check("9.0.1+12", "9.0.2+12"); + check("9.0.2+60", "9.0.2+100"); + } + + @Test + public void differentJdk() { + check("1.7.0", "1.8.0u60"); + check("1.8.1u40", "9.0.2+12"); + } + + @Test + public void longer() { + check("1.8.0", "1.8.0.1"); + } + + @Test + public void longerEquality() { + checkEqual("1.8.0", "1.8.0"); + checkEqual("1.8.0u33", "1.8.0u33"); + checkEqual("5", "5"); + } + + private static void check(String v1, String v2) { + assertThat(new JavaVersion(v1).compareTo(new JavaVersion(v2))).isNegative(); + } + + private static void checkEqual(String v1, String v2) { + assertThat(new JavaVersion(v1).compareTo(new JavaVersion(v2))).isZero(); + } + +}
\ No newline at end of file diff --git a/robolectric-sandbox/src/test/java/org/robolectric/RealApisTest.java b/robolectric-sandbox/src/test/java/org/robolectric/RealApisTest.java new file mode 100644 index 000000000..05b94aeb4 --- /dev/null +++ b/robolectric-sandbox/src/test/java/org/robolectric/RealApisTest.java @@ -0,0 +1,44 @@ +package org.robolectric; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.annotation.Implements; +import org.robolectric.annotation.internal.Instrument; +import org.robolectric.internal.InstrumentingTestRunner; +import org.robolectric.internal.bytecode.RoboConfig; +import org.robolectric.testing.Pony; + +import static org.junit.Assert.assertEquals; + +@RunWith(InstrumentingTestRunner.class) +public class RealApisTest { + @Test + @RoboConfig(shadows = {ShimmeryShadowPony.class}) + public void whenShadowHandlerIsInRealityBasedMode_shouldNotCallRealForUnshadowedMethod() throws Exception { + assertEquals("Off I saunter to the salon!", new Pony().saunter("the salon")); + } + + @Implements(Pony.class) + public static class ShimmeryShadowPony extends Pony.ShadowPony { + } + + @Test + @RoboConfig(shadows = {ShadowOfClassWithSomeConstructors.class}) + public void shouldCallOriginalConstructorBodySomehow() throws Exception { + ClassWithSomeConstructors o = new ClassWithSomeConstructors("my name"); + assertEquals("my name", o.name); + } + + @Instrument + public static class ClassWithSomeConstructors { + public String name; + + public ClassWithSomeConstructors(String name) { + this.name = name; + } + } + + @Implements(ClassWithSomeConstructors.class) + public static class ShadowOfClassWithSomeConstructors { + } +} diff --git a/robolectric-sandbox/src/test/java/org/robolectric/RobolectricInternalsTest.java b/robolectric-sandbox/src/test/java/org/robolectric/RobolectricInternalsTest.java new file mode 100644 index 000000000..d3d9523e8 --- /dev/null +++ b/robolectric-sandbox/src/test/java/org/robolectric/RobolectricInternalsTest.java @@ -0,0 +1,154 @@ +package org.robolectric; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; +import org.robolectric.annotation.internal.Instrument; +import org.robolectric.internal.InstrumentingTestRunner; +import org.robolectric.internal.Shadow; +import org.robolectric.internal.ShadowExtractor; +import org.robolectric.internal.bytecode.RoboConfig; +import org.robolectric.util.ReflectionHelpers.ClassParameter; + +import static org.assertj.core.api.Assertions.assertThat; + +@RoboConfig(shadows={ RobolectricInternalsTest.ShadowConstructors.class }) +@RunWith(InstrumentingTestRunner.class) +public class RobolectricInternalsTest { + + private static final String PARAM1 = "param1"; + private static final Byte PARAM2 = (byte) 24; + private static final Long PARAM3 = (long) 10122345; + + @Test + public void getConstructor_withNoParams() { + Constructors a = new Constructors(); + ShadowConstructors sa = shadowOf(a); + + assertThat(a.constructorCalled).isFalse(); + assertThat(sa.shadowConstructorCalled).isTrue(); + + Shadow.invokeConstructor(Constructors.class, a); + assertThat(a.constructorCalled).isTrue(); + } + + @Test + public void getConstructor_withOneClassParam() { + Constructors a = new Constructors(PARAM1); + ShadowConstructors sa = shadowOf(a); + + assertThat(a.param11).isNull(); + assertThat(sa.shadowParam11).isEqualTo(PARAM1); + + Shadow.invokeConstructor(Constructors.class, a, ClassParameter.from(String.class, PARAM1)); + assertThat(a.param11).isEqualTo(PARAM1); + } + + @Test + public void getConstructor_withTwoClassParams() { + Constructors a = new Constructors(PARAM1, PARAM2); + ShadowConstructors sa = shadowOf(a); + + assertThat(a.param21).isNull(); + assertThat(a.param22).isNull(); + assertThat(sa.shadowParam21).isEqualTo(PARAM1); + assertThat(sa.shadowParam22).isEqualTo(PARAM2); + + Shadow.invokeConstructor(Constructors.class, a, ClassParameter.from(String.class, PARAM1), ClassParameter.from(Byte.class, PARAM2)); + assertThat(a.param21).isEqualTo(PARAM1); + assertThat(a.param22).isEqualTo(PARAM2); + } + + @Test + public void getConstructor_withThreeClassParams() { + Constructors a = new Constructors(PARAM1, PARAM2, PARAM3); + ShadowConstructors sa = shadowOf(a); + + assertThat(a.param31).isNull(); + assertThat(a.param32).isNull(); + assertThat(a.param33).isNull(); + assertThat(sa.shadowParam31).isEqualTo(PARAM1); + assertThat(sa.shadowParam32).isEqualTo(PARAM2); + assertThat(sa.shadowParam33).isEqualTo(PARAM3); + + Shadow.invokeConstructor(Constructors.class, a, ClassParameter.from(String.class, PARAM1), ClassParameter.from(Byte.class, PARAM2), ClassParameter.from(Long.class, PARAM3)); + assertThat(a.param31).isEqualTo(PARAM1); + assertThat(a.param32).isEqualTo(PARAM2); + assertThat(a.param33).isEqualTo(PARAM3); + } + + private static ShadowConstructors shadowOf(Constructors realObject) { + Object shadow = ShadowExtractor.extract(realObject); + assertThat(shadow).isNotNull().isInstanceOf(ShadowConstructors.class); + return (ShadowConstructors) shadow; + } + + @Instrument + public static class Constructors { + public boolean constructorCalled = false; + public String param11 = null; + + public String param21 = null; + public Byte param22 = null; + + public String param31 = null; + public Byte param32 = null; + public Long param33 = null; + + public Constructors() { + constructorCalled = true; + } + + public Constructors(String param) { + param11 = param; + } + + public Constructors(String param1, Byte param2) { + param21 = param1; + param22 = param2; + } + + public Constructors(String param1, Byte param2, Long param3) { + param31 = param1; + param32 = param2; + param33 = param3; + } + } + + @Implements(Constructors.class) + public static class ShadowConstructors { + public boolean shadowConstructorCalled = false; + public String shadowParam11 = null; + + public String shadowParam21 = null; + public Byte shadowParam22 = null; + + public String shadowParam31 = null; + public Byte shadowParam32 = null; + public Long shadowParam33 = null; + + @Implementation + public void __constructor__() { + shadowConstructorCalled = true; + } + + @Implementation + public void __constructor__(String param) { + shadowParam11 = param; + } + + @Implementation + public void __constructor__(String param1, Byte param2) { + shadowParam21 = param1; + shadowParam22 = param2; + } + + @Implementation + public void __constructor__(String param1, Byte param2, Long param3) { + shadowParam31 = param1; + shadowParam32 = param2; + shadowParam33 = param3; + } + } +} diff --git a/robolectric-sandbox/src/test/java/org/robolectric/ShadowWranglerIntegrationTest.java b/robolectric-sandbox/src/test/java/org/robolectric/ShadowWranglerIntegrationTest.java new file mode 100644 index 000000000..5ff4a5c5a --- /dev/null +++ b/robolectric-sandbox/src/test/java/org/robolectric/ShadowWranglerIntegrationTest.java @@ -0,0 +1,325 @@ +package org.robolectric; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.internal.InstrumentingTestRunner; +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; +import org.robolectric.annotation.RealObject; +import org.robolectric.annotation.internal.Instrument; +import org.robolectric.internal.ShadowExtractor; +import org.robolectric.internal.bytecode.RoboConfig; +import org.robolectric.internal.bytecode.ShadowWrangler; +import org.robolectric.testing.Foo; +import org.robolectric.testing.ShadowFoo; + +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.*; + +@RunWith(InstrumentingTestRunner.class) +public class ShadowWranglerIntegrationTest { + private String name; + + @Before + public void setUp() throws Exception { + name = "context"; + } + + @Test + @RoboConfig(shadows = {ShadowForAClassWithDefaultConstructor_HavingNoConstructorDelegate.class}) + public void testConstructorInvocation_WithDefaultConstructorAndNoConstructorDelegateOnShadowClass() throws Exception { + AClassWithDefaultConstructor instance = new AClassWithDefaultConstructor(); + assertThat(ShadowExtractor.extract(instance)).isExactlyInstanceOf(ShadowForAClassWithDefaultConstructor_HavingNoConstructorDelegate.class); + assertThat(instance.initialized).isTrue(); + } + + @Test + @RoboConfig(shadows = { ShadowFoo.class }) + public void testConstructorInvocation() throws Exception { + Foo foo = new Foo(name); + assertSame(name, shadowOf(foo).name); + } + + @Test + @RoboConfig(shadows = {ShadowFoo.class}) + public void testRealObjectAnnotatedFieldsAreSetBeforeConstructorIsCalled() throws Exception { + Foo foo = new Foo(name); + assertSame(name, shadowOf(foo).name); + assertSame(foo, shadowOf(foo).realFooField); + + assertSame(foo, shadowOf(foo).realFooInConstructor); + assertSame(foo, shadowOf(foo).realFooInParentConstructor); + } + + @Test + @RoboConfig(shadows = {ShadowFoo.class}) + public void testMethodDelegation() throws Exception { + Foo foo = new Foo(name); + assertSame(name, foo.getName()); + } + + @Test + @RoboConfig(shadows = {WithEquals.class}) + public void testEqualsMethodDelegation() throws Exception { + Foo foo1 = new Foo(name); + Foo foo2 = new Foo(name); + assertEquals(foo1, foo2); + } + + @Test + @RoboConfig(shadows = {WithEquals.class}) + public void testHashCodeMethodDelegation() throws Exception { + Foo foo = new Foo(name); + assertEquals(42, foo.hashCode()); + } + + @Test + @RoboConfig(shadows = {WithToString.class}) + public void testToStringMethodDelegation() throws Exception { + Foo foo = new Foo(name); + assertEquals("the expected string", foo.toString()); + } + + @Test + @RoboConfig(shadows = {ShadowFoo.class}) + public void testShadowSelectionSearchesSuperclasses() throws Exception { + TextFoo textFoo = new TextFoo(name); + assertEquals(ShadowFoo.class, ShadowExtractor.extract(textFoo).getClass()); + } + + @Test + @RoboConfig(shadows = {ShadowFoo.class, ShadowTextFoo.class}) + public void shouldUseMostSpecificShadow() throws Exception { + TextFoo textFoo = new TextFoo(name); + assertThat(shadowOf(textFoo)).isInstanceOf(ShadowTextFoo.class); + } + + @Test + public void testPrimitiveArrays() throws Exception { + Class<?> objArrayClass = ShadowWrangler.loadClass("java.lang.Object[]", getClass().getClassLoader()); + assertTrue(objArrayClass.isArray()); + assertEquals(Object.class, objArrayClass.getComponentType()); + + Class<?> intArrayClass = ShadowWrangler.loadClass("int[]", getClass().getClassLoader()); + assertTrue(intArrayClass.isArray()); + assertEquals(Integer.TYPE, intArrayClass.getComponentType()); + } + + @Test + @RoboConfig(shadows = ShadowThrowInShadowMethod.class) + public void shouldRemoveNoiseFromShadowedStackTraces() throws Exception { + ThrowInShadowMethod instance = new ThrowInShadowMethod(); + + Exception e = null; + try { + instance.method(); + } catch (Exception e1) { + e = e1; + } + + assertNotNull(e); + assertEquals(IOException.class, e.getClass()); + assertEquals("fake exception", e.getMessage()); + StackTraceElement[] stackTrace = e.getStackTrace(); + + assertThat(stackTrace[0].getClassName()).isEqualTo(ShadowThrowInShadowMethod.class.getName()); + assertThat(stackTrace[0].getMethodName()).isEqualTo("method"); + assertThat(stackTrace[0].getLineNumber()).isGreaterThan(0); + + assertThat(stackTrace[1].getClassName()).isEqualTo(ThrowInShadowMethod.class.getName()); + assertThat(stackTrace[1].getMethodName()).isEqualTo("method"); + assertThat(stackTrace[1].getLineNumber()).isLessThan(0); + + assertThat(stackTrace[2].getClassName()).isEqualTo(ShadowWranglerIntegrationTest.class.getName()); + assertThat(stackTrace[2].getMethodName()).isEqualTo("shouldRemoveNoiseFromShadowedStackTraces"); + assertThat(stackTrace[2].getLineNumber()).isGreaterThan(0); + } + + @Instrument + public static class ThrowInShadowMethod { + public void method() throws IOException { + } + } + + @Implements(ThrowInShadowMethod.class) + public static class ShadowThrowInShadowMethod { + public void method() throws IOException { + throw new IOException("fake exception"); + } + } + + + @Test + @RoboConfig(shadows = ShadowThrowInRealMethod.class) + public void shouldRemoveNoiseFromUnshadowedStackTraces() throws Exception { + ThrowInRealMethod instance = new ThrowInRealMethod(); + + Exception e = null; + try { + instance.method(); + } catch (Exception e1) { + e = e1; + } + + assertNotNull(e); + assertEquals(IOException.class, e.getClass()); + assertEquals("fake exception", e.getMessage()); + StackTraceElement[] stackTrace = e.getStackTrace(); + + assertThat(stackTrace[0].getClassName()).isEqualTo(ThrowInRealMethod.class.getName()); + assertThat(stackTrace[0].getMethodName()).isEqualTo("method"); + assertThat(stackTrace[0].getLineNumber()).isGreaterThan(0); + + assertThat(stackTrace[1].getClassName()).isEqualTo(ShadowWranglerIntegrationTest.class.getName()); + assertThat(stackTrace[1].getMethodName()).isEqualTo("shouldRemoveNoiseFromUnshadowedStackTraces"); + assertThat(stackTrace[1].getLineNumber()).isGreaterThan(0); + } + + @Instrument + public static class ThrowInRealMethod { + public void method() throws IOException { + throw new IOException("fake exception"); + } + } + + @Implements(ThrowInRealMethod.class) + public static class ShadowThrowInRealMethod { + } + + @Test @RoboConfig(shadows = {ShadowOfChildWithInheritance.class, ShadowOfParent.class}) + public void whenInheritanceIsEnabled_shouldUseShadowSuperclassMethods() throws Exception { + assertThat(new Child().get()).isEqualTo("from shadow of parent"); + } + + @Test @RoboConfig(shadows = {ShadowOfChildWithoutInheritance.class, ShadowOfParent.class}) + public void whenInheritanceIsDisabled_shouldUseShadowSuperclassMethods() throws Exception { + assertThat(new Child().get()).isEqualTo("from child (from shadow of parent)"); + } + + @Instrument + public static class Parent { + public String get() { + return "from parent"; + } + } + + @Instrument + public static class Child extends Parent { + public String get() { + return "from child (" + super.get() + ")"; + } + } + + @Implements(Parent.class) + public static class ShadowOfParent { + @Implementation + public String get() { + return "from shadow of parent"; + } + } + + @Implements(value = Child.class, inheritImplementationMethods = true) + public static class ShadowOfChildWithInheritance extends ShadowOfParent { + } + + @Implements(value = Child.class, inheritImplementationMethods = false) + public static class ShadowOfChildWithoutInheritance extends ShadowOfParent { + } + + private ShadowFoo shadowOf(Foo foo) { + return (ShadowFoo) ShadowExtractor.extract(foo); + } + + private ShadowTextFoo shadowOf(TextFoo foo) { + return (ShadowTextFoo) ShadowExtractor.extract(foo); + } + + @Implements(Foo.class) + public static class WithEquals { + @SuppressWarnings("UnusedDeclaration") + public void __constructor__(String s) { + } + + @Override + public boolean equals(Object o) { + return true; + } + + + @Override + public int hashCode() { + return 42; + } + + } + + @Implements(Foo.class) + public static class WithToString { + @SuppressWarnings("UnusedDeclaration") + public void __constructor__(String s) { + } + + @Override + public String toString() { + return "the expected string"; + } + } + + @Implements(TextFoo.class) + public static class ShadowTextFoo extends ShadowFoo { + } + + @Instrument + public static class TextFoo extends Foo { + public TextFoo(String s) { + super(s); + } + } + + @Implements(Foo.class) + public static class ShadowFooParent { + @RealObject + private Foo realFoo; + public Foo realFooInParentConstructor; + + public void __constructor__(String name) { + realFooInParentConstructor = realFoo; + } + } + + @Instrument + public static class AClassWithDefaultConstructor { + public boolean initialized; + + public AClassWithDefaultConstructor() { + initialized = true; + } + } + + @Implements(AClassWithDefaultConstructor.class) + public static class ShadowForAClassWithDefaultConstructor_HavingNoConstructorDelegate { + } + + @RoboConfig(shadows = ShadowAClassWithDifficultArgs.class) + @Test public void shouldAllowLooseSignatureMatches() throws Exception { + assertThat(new AClassWithDifficultArgs().aMethod("bc")).isEqualTo("abc"); + } + + @Implements(value = AClassWithDifficultArgs.class, looseSignatures = true) + public static class ShadowAClassWithDifficultArgs { + @Implementation + public Object aMethod(Object s) { + return "a" + s; + } + } + + @Instrument + public static class AClassWithDifficultArgs { + public CharSequence aMethod(CharSequence s) { + return s; + } + } +} diff --git a/robolectric-sandbox/src/test/java/org/robolectric/ShadowingTest.java b/robolectric-sandbox/src/test/java/org/robolectric/ShadowingTest.java new file mode 100644 index 000000000..de5d5b0a1 --- /dev/null +++ b/robolectric-sandbox/src/test/java/org/robolectric/ShadowingTest.java @@ -0,0 +1,248 @@ +package org.robolectric; + +import org.assertj.core.api.Assertions; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; +import org.robolectric.annotation.internal.Instrument; +import org.robolectric.internal.InstrumentingTestRunner; +import org.robolectric.internal.Shadow; +import org.robolectric.internal.ShadowConstants; +import org.robolectric.internal.bytecode.RoboConfig; +import org.robolectric.testing.AFinalClass; +import org.robolectric.testing.Pony; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.mock; + +@RunWith(InstrumentingTestRunner.class) +public class ShadowingTest { + + @Test + @RoboConfig(shadows = {ShadowAccountManagerForTests.class}) + public void testStaticMethodsAreDelegated() throws Exception { + Object arg = mock(Object.class); + AccountManager.get(arg); + assertThat(ShadowAccountManagerForTests.wasCalled).isTrue(); + assertThat(ShadowAccountManagerForTests.arg).isSameAs(arg); + } + + @Implements(AccountManager.class) + public static class ShadowAccountManagerForTests { + public static boolean wasCalled = false; + public static Object arg; + + public static AccountManager get(Object arg) { + wasCalled = true; + ShadowAccountManagerForTests.arg = arg; + return mock(AccountManager.class); + } + } + + static class Context { + } + + static class AccountManager { + public static AccountManager get(Object arg) { + return null; + } + } + + @Test + @RoboConfig(shadows = {ShadowClassWithProtectedMethod.class}) + public void testProtectedMethodsAreDelegated() throws Exception { + ClassWithProtectedMethod overlay = new ClassWithProtectedMethod(); + assertEquals("shadow name", overlay.getName()); + } + + @Implements(ClassWithProtectedMethod.class) + public static class ShadowClassWithProtectedMethod { + @Implementation + public String getName() { + return "shadow name"; + } + } + + @Instrument + public static class ClassWithProtectedMethod { + protected String getName() { + return "protected name"; + } + } + + @Test + @RoboConfig(shadows = {ShadowPaintForTests.class}) + public void testNativeMethodsAreDelegated() throws Exception { + Paint paint = new Paint(); + paint.setColor(1234); + + Assertions.assertThat(paint.getColor()).isEqualTo(1234); + } + + @Instrument + static class Paint { + public native void setColor(int color); + public native int getColor(); + } + + @Implements(Paint.class) + public static class ShadowPaintForTests { + private int color; + + @Implementation + public void setColor(int color) { + this.color = color; + } + + @Implementation + public int getColor() { + return color; + } + } + + @Implements(ClassWithNoDefaultConstructor.class) + public static class ShadowForClassWithNoDefaultConstructor { + public static boolean shadowDefaultConstructorCalled = false; + public static boolean shadowDefaultConstructorImplementorCalled = false; + + public ShadowForClassWithNoDefaultConstructor() { + this.shadowDefaultConstructorCalled = true; + } + + public void __constructor__() { + shadowDefaultConstructorImplementorCalled = true; + } + } + + @Instrument @SuppressWarnings({"UnusedDeclaration"}) + public static class ClassWithNoDefaultConstructor { + ClassWithNoDefaultConstructor(String string) { + } + } + + @Test + @RoboConfig(shadows = {Pony.ShadowPony.class}) + public void directlyOn_shouldCallThroughToOriginalMethodBody() throws Exception { + Pony pony = new Pony(); + + assertEquals("Fake whinny! You're on my neck!", pony.ride("neck")); + assertEquals("Whinny! You're on my neck!", Shadow.directlyOn(pony, Pony.class).ride("neck")); + + assertEquals("Fake whinny! You're on my haunches!", pony.ride("haunches")); + } + + @Test + @RoboConfig(shadows = {Pony.ShadowPony.class}) + public void shouldCallRealForUnshadowedMethod() throws Exception { + assertEquals("Off I saunter to the salon!", new Pony().saunter("the salon")); + } + + static class TextView { + } + + static class ColorStateList { + public ColorStateList(int[][] ints, int[] ints1) { + } + } + + static class TypedArray { + } + + @Implements(TextView.class) + public static class TextViewWithDummyGetTextColorsMethod { + public static ColorStateList getTextColors(Context context, TypedArray attrs) { + return new ColorStateList(new int[0][0], new int[0]); + } + } + + @Test + @RoboConfig(shadows = ShadowOfClassWithSomeConstructors.class) + public void shouldGenerateSeparatedConstructorBodies() throws Exception { + ClassWithSomeConstructors o = new ClassWithSomeConstructors("my name"); + assertNull(o.name); + + Method realConstructor = o.getClass().getDeclaredMethod(ShadowConstants.CONSTRUCTOR_METHOD_NAME, String.class); + realConstructor.setAccessible(true); + realConstructor.invoke(o, "my name"); + assertEquals("my name", o.name); + } + + @Instrument + public static class ClassWithSomeConstructors { + public String name; + + public ClassWithSomeConstructors(String name) { + this.name = name; + } + } + + @Implements(ClassWithSomeConstructors.class) + public static class ShadowOfClassWithSomeConstructors { + @SuppressWarnings("UnusedDeclaration") + public void __constructor__(String s) { + } + } + + @Test + @RoboConfig(shadows = {ShadowApiImplementedClass.class}) + public void withNonApiSubclassesWhichExtendApi_shouldStillBeInvoked() throws Exception { + assertEquals("did foo", new NonApiSubclass().doSomething("foo")); + } + + public static class NonApiSubclass extends ApiImplementedClass { + public String doSomething(String value) { + return "did " + value; + } + } + + @Instrument + public static class ApiImplementedClass { + } + + @Implements(ApiImplementedClass.class) + public static class ShadowApiImplementedClass { + } + + @Test + public void shouldNotInstrumentClassIfNotAddedToConfig() { + assertEquals(1, new NonInstrumentedClass().plus(0)); + } + + @Test + @RoboConfig(shadows = {ShadowNonInstrumentedClass.class}) + public void shouldInstrumentClassIfAddedToConfig() { + assertEquals(2, new NonInstrumentedClass().plus(0)); + } + + public static class NonInstrumentedClass { + public int plus(int x) { + return x + 1; + } + } + + @Implements(NonInstrumentedClass.class) + public static class ShadowNonInstrumentedClass { + @Implementation + public int plus(int x) { + return x + 2; + } + } + + public void shouldNotInstrumentPackageIfNotAddedToConfig() throws Exception { + Class<?> clazz = Class.forName(AFinalClass.class.getName()); + assertEquals(1, clazz.getModifiers() & Modifier.FINAL); + } + + @Test + @RoboConfig(instrumentedPackages = {"org.robolectric.testing"}) + public void shouldInstrumentPackageIfAddedToConfig() throws Exception { + Class<?> clazz = Class.forName(AFinalClass.class.getName()); + assertEquals(0, clazz.getModifiers() & Modifier.FINAL); + } +} diff --git a/robolectric-sandbox/src/test/java/org/robolectric/StaticInitializerTest.java b/robolectric-sandbox/src/test/java/org/robolectric/StaticInitializerTest.java new file mode 100644 index 000000000..ed2f2f01e --- /dev/null +++ b/robolectric-sandbox/src/test/java/org/robolectric/StaticInitializerTest.java @@ -0,0 +1,68 @@ +package org.robolectric; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.annotation.Implements; +import org.robolectric.annotation.internal.Instrument; +import org.robolectric.internal.InstrumentingTestRunner; +import org.robolectric.internal.bytecode.RoboConfig; +import org.robolectric.internal.bytecode.RobolectricInternals; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +@RunWith(InstrumentingTestRunner.class) +public class StaticInitializerTest { + @Test + public void whenClassIsUnshadowed_shouldPerformStaticInitialization() throws Exception { + assertEquals("Floyd", ClassWithStaticInitializerA.name); + } + + @Instrument + public static class ClassWithStaticInitializerA { + static String name = "Floyd"; + } + + + @Test + @RoboConfig(shadows = {ShadowClassWithoutStaticInitializerOverride.class}) + public void whenClassHasShadowWithoutOverrideMethod_shouldPerformStaticInitialization() throws Exception { + assertEquals("Floyd", ClassWithStaticInitializerB.name); + + RobolectricInternals.performStaticInitialization(ClassWithStaticInitializerB.class); + assertEquals("Floyd", ClassWithStaticInitializerB.name); + } + + @Instrument public static class ClassWithStaticInitializerB { + public static String name = "Floyd"; + } + + @Implements(ClassWithStaticInitializerB.class) public static class ShadowClassWithoutStaticInitializerOverride { + } + + @Test + @RoboConfig(shadows = {ShadowClassWithStaticInitializerOverride.class}) + public void whenClassHasShadowWithOverrideMethod_shouldDeferStaticInitialization() throws Exception { + assertFalse(ShadowClassWithStaticInitializerOverride.initialized); + assertEquals(null, ClassWithStaticInitializerC.name); + assertTrue(ShadowClassWithStaticInitializerOverride.initialized); + + RobolectricInternals.performStaticInitialization(ClassWithStaticInitializerC.class); + assertEquals("Floyd", ClassWithStaticInitializerC.name); + } + + @Instrument public static class ClassWithStaticInitializerC { + public static String name = "Floyd"; + } + + @Implements(ClassWithStaticInitializerC.class) + public static class ShadowClassWithStaticInitializerOverride { + public static boolean initialized = false; + + @SuppressWarnings("UnusedDeclaration") + public static void __staticInitializer__() { + initialized = true; + } + } +} diff --git a/robolectric-sandbox/src/test/java/org/robolectric/ThreadSafetyTest.java b/robolectric-sandbox/src/test/java/org/robolectric/ThreadSafetyTest.java new file mode 100644 index 000000000..7f02b7115 --- /dev/null +++ b/robolectric-sandbox/src/test/java/org/robolectric/ThreadSafetyTest.java @@ -0,0 +1,55 @@ +package org.robolectric; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; +import org.robolectric.annotation.RealObject; +import org.robolectric.annotation.internal.Instrument; +import org.robolectric.internal.InstrumentingTestRunner; +import org.robolectric.internal.Shadow; +import org.robolectric.internal.ShadowExtractor; +import org.robolectric.internal.bytecode.RoboConfig; + +import java.lang.reflect.Field; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(InstrumentingTestRunner.class) +public class ThreadSafetyTest { + @Test + @RoboConfig(shadows = {InstrumentedThreadShadow.class}) + public void shadowCreationShouldBeThreadsafe() throws Exception { + Field field = InstrumentedThread.class.getDeclaredField("shadowFromOtherThread"); + field.setAccessible(true); + + for (int i = 0; i < 100; i++) { // :-( + InstrumentedThread instrumentedThread = new InstrumentedThread(); + instrumentedThread.start(); + Object shadowFromThisThread = ShadowExtractor.extract(instrumentedThread); + + instrumentedThread.join(); + Object shadowFromOtherThread = field.get(instrumentedThread); + assertThat(shadowFromThisThread).isSameAs(shadowFromOtherThread); + } + } + + @Instrument + public static class InstrumentedThread extends Thread { + InstrumentedThreadShadow shadowFromOtherThread; + + @Override + public void run() { + shadowFromOtherThread = (InstrumentedThreadShadow) ShadowExtractor.extract(this); + } + } + + @Implements(InstrumentedThread.class) + public static class InstrumentedThreadShadow { + @RealObject InstrumentedThread realObject; + @Implementation + public void run() { + Shadow.directlyOn(realObject, InstrumentedThread.class, "run"); + } + } +} diff --git a/robolectric-sandbox/src/test/java/org/robolectric/internal/ProxyMakerTest.java b/robolectric-sandbox/src/test/java/org/robolectric/internal/ProxyMakerTest.java new file mode 100644 index 000000000..5a90a6899 --- /dev/null +++ b/robolectric-sandbox/src/test/java/org/robolectric/internal/ProxyMakerTest.java @@ -0,0 +1,65 @@ +package org.robolectric.internal; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class ProxyMakerTest { + private static final ProxyMaker.MethodMapper IDENTITY_NAME = new ProxyMaker.MethodMapper() { + @Override public String getName(String className, String methodName) { + return methodName; + } + }; + + @Test + public void proxyCall() { + ProxyMaker maker = new ProxyMaker(IDENTITY_NAME); + + Thing mock = mock(Thing.class); + Thing proxy = maker.createProxyFactory(Thing.class).createProxy(Thing.class, mock); + assertThat(proxy.getClass()).isNotSameAs(Thing.class); + + proxy.returnNothing(); + verify(mock).returnNothing(); + + when(mock.returnInt()).thenReturn(42); + assertThat(proxy.returnInt()).isEqualTo(42); + verify(mock).returnInt(); + + proxy.argument("hello"); + verify(mock).argument("hello"); + } + + @Test + public void cachesProxyClass() { + ProxyMaker maker = new ProxyMaker(IDENTITY_NAME); + Thing thing1 = mock(Thing.class); + Thing thing2 = mock(Thing.class); + + Thing proxy1 = maker.createProxy(Thing.class, thing1); + Thing proxy2 = maker.createProxy(Thing.class, thing2); + + assertThat(proxy1.getClass()).isSameAs(proxy2.getClass()); + } + + public class Thing { + public Thing() { + throw new UnsupportedOperationException(); + } + + public void returnNothing() { + throw new UnsupportedOperationException(); + } + + public int returnInt() { + throw new UnsupportedOperationException(); + } + + public void argument(String arg) { + throw new UnsupportedOperationException(); + } + } +} diff --git a/robolectric-sandbox/src/test/java/org/robolectric/testing/AChild.java b/robolectric-sandbox/src/test/java/org/robolectric/testing/AChild.java new file mode 100644 index 000000000..d4c76bf4e --- /dev/null +++ b/robolectric-sandbox/src/test/java/org/robolectric/testing/AChild.java @@ -0,0 +1,7 @@ +package org.robolectric.testing; + +import org.robolectric.annotation.internal.Instrument; + +@Instrument +public class AChild extends AParent { +} diff --git a/robolectric-sandbox/src/test/java/org/robolectric/testing/AClassThatCallsAMethodReturningAForgettableClass.java b/robolectric-sandbox/src/test/java/org/robolectric/testing/AClassThatCallsAMethodReturningAForgettableClass.java new file mode 100644 index 000000000..74d57b1b8 --- /dev/null +++ b/robolectric-sandbox/src/test/java/org/robolectric/testing/AClassThatCallsAMethodReturningAForgettableClass.java @@ -0,0 +1,15 @@ +package org.robolectric.testing; + +import org.robolectric.annotation.internal.Instrument; + +@SuppressWarnings("UnusedDeclaration") +@Instrument +public class AClassThatCallsAMethodReturningAForgettableClass { + public void callSomeMethod() { + AClassToForget forgettableClass = getAForgettableClass(); + } + + public AClassToForget getAForgettableClass() { + throw new RuntimeException("should never be called!"); + } +} diff --git a/robolectric-sandbox/src/test/java/org/robolectric/testing/AClassThatExtendsAClassWithFinalEqualsHashCode.java b/robolectric-sandbox/src/test/java/org/robolectric/testing/AClassThatExtendsAClassWithFinalEqualsHashCode.java new file mode 100644 index 000000000..c34b0b668 --- /dev/null +++ b/robolectric-sandbox/src/test/java/org/robolectric/testing/AClassThatExtendsAClassWithFinalEqualsHashCode.java @@ -0,0 +1,7 @@ +package org.robolectric.testing; + +import org.robolectric.annotation.internal.Instrument; + +@Instrument +public class AClassThatExtendsAClassWithFinalEqualsHashCode extends AClassWithFinalEqualsHashCode { +} diff --git a/robolectric-sandbox/src/test/java/org/robolectric/testing/AClassThatRefersToAForgettableClass.java b/robolectric-sandbox/src/test/java/org/robolectric/testing/AClassThatRefersToAForgettableClass.java new file mode 100644 index 000000000..63310609c --- /dev/null +++ b/robolectric-sandbox/src/test/java/org/robolectric/testing/AClassThatRefersToAForgettableClass.java @@ -0,0 +1,20 @@ +package org.robolectric.testing; + +import org.robolectric.annotation.internal.Instrument; + +@SuppressWarnings("UnusedDeclaration") +@Instrument +public class AClassThatRefersToAForgettableClass { + public AClassToForget someField; + public AClassToForget[] someFields; + + public String interactWithForgettableClass() { + AClassToForget aClassToForget = new AClassToForget(); + return aClassToForget.forgettableMethod() + ", " + aClassToForget.memorableMethod(); + } + + public String interactWithForgettableStaticMethod() { + return AClassToForget.memorableStaticMethod() + " forget this: " + AClassToForget.forgettableStaticMethod(); + } + +} diff --git a/robolectric-sandbox/src/test/java/org/robolectric/testing/AClassThatRefersToAForgettableClassInItsConstructor.java b/robolectric-sandbox/src/test/java/org/robolectric/testing/AClassThatRefersToAForgettableClassInItsConstructor.java new file mode 100644 index 000000000..f64cb099e --- /dev/null +++ b/robolectric-sandbox/src/test/java/org/robolectric/testing/AClassThatRefersToAForgettableClassInItsConstructor.java @@ -0,0 +1,13 @@ +package org.robolectric.testing; + +import org.robolectric.annotation.internal.Instrument; + +@SuppressWarnings("UnusedDeclaration") +@Instrument +public class AClassThatRefersToAForgettableClassInItsConstructor { + public final AClassToForget aClassToForget; + + public AClassThatRefersToAForgettableClassInItsConstructor() { + aClassToForget = null; + } +} diff --git a/robolectric-sandbox/src/test/java/org/robolectric/testing/AClassThatRefersToAForgettableClassInMethodCalls.java b/robolectric-sandbox/src/test/java/org/robolectric/testing/AClassThatRefersToAForgettableClassInMethodCalls.java new file mode 100644 index 000000000..6d920745a --- /dev/null +++ b/robolectric-sandbox/src/test/java/org/robolectric/testing/AClassThatRefersToAForgettableClassInMethodCalls.java @@ -0,0 +1,15 @@ +package org.robolectric.testing; + +import org.robolectric.annotation.internal.Instrument; + +@SuppressWarnings("UnusedDeclaration") +@Instrument +public class AClassThatRefersToAForgettableClassInMethodCalls { + AClassToForget aMethod(int a, AClassToForget aClassToForget, String b) { + return null; + } + + AClassToForget[] anotherMethod(int a, AClassToForget[] aClassToForget, String b) { + return null; + } +} diff --git a/robolectric-sandbox/src/test/java/org/robolectric/testing/AClassThatRefersToAForgettableClassInMethodCallsReturningPrimitive.java b/robolectric-sandbox/src/test/java/org/robolectric/testing/AClassThatRefersToAForgettableClassInMethodCallsReturningPrimitive.java new file mode 100644 index 000000000..245f0ca2e --- /dev/null +++ b/robolectric-sandbox/src/test/java/org/robolectric/testing/AClassThatRefersToAForgettableClassInMethodCallsReturningPrimitive.java @@ -0,0 +1,59 @@ +package org.robolectric.testing; + +import org.robolectric.annotation.internal.Instrument; + +@SuppressWarnings("UnusedDeclaration") +@Instrument +public class AClassThatRefersToAForgettableClassInMethodCallsReturningPrimitive { + byte byteMethod() { + return AClassToForget.byteReturningMethod(); + } + + byte[] byteArrayMethod() { + return AClassToForget.byteArrayReturningMethod(); + } + + int intMethod() { + return AClassToForget.intReturningMethod(); + } + + int[] intArrayMethod() { + return AClassToForget.intArrayReturningMethod(); + } + + long longMethod() { + return AClassToForget.longReturningMethod("str", 123, 456); + } + + long[] longArrayMethod() { + return AClassToForget.longArrayReturningMethod(); + } + + float floatMethod() { + return AClassToForget.floatReturningMethod(); + } + + float[] floatArrayMethod() { + return AClassToForget.floatArrayReturningMethod(); + } + + double doubleMethod() { + return AClassToForget.doubleReturningMethod(); + } + + double[] doubleArrayMethod() { + return AClassToForget.doubleArrayReturningMethod(); + } + + short shortMethod() { + return AClassToForget.shortReturningMethod(); + } + + short[] shortArrayMethod() { + return AClassToForget.shortArrayReturningMethod(); + } + + void voidReturningMethod() { + AClassToForget.voidReturningMethod(); + } +} diff --git a/robolectric-sandbox/src/test/java/org/robolectric/testing/AClassToForget.java b/robolectric-sandbox/src/test/java/org/robolectric/testing/AClassToForget.java new file mode 100644 index 000000000..f2b689c03 --- /dev/null +++ b/robolectric-sandbox/src/test/java/org/robolectric/testing/AClassToForget.java @@ -0,0 +1,70 @@ +package org.robolectric.testing; + +public class AClassToForget { + public String memorableMethod() { + return "get this!"; + } + + public String forgettableMethod() { + return "shouldn't get this!"; + } + + public static String memorableStaticMethod() { + return "yess?"; + } + + public static String forgettableStaticMethod() { + return "noooo!"; + } + + public static int intReturningMethod() { + return 1; + } + + public static int[] intArrayReturningMethod() { + return new int[0]; + } + + public static long longReturningMethod(String str, int i, long l) { + return 1; + } + + public static long[] longArrayReturningMethod() { + return new long[0]; + } + + public static byte byteReturningMethod() { + return 0; + } + + public static byte[] byteArrayReturningMethod() { + return new byte[0]; + } + + public static float floatReturningMethod() { + return 0f; + } + + public static float[] floatArrayReturningMethod() { + return new float[0]; + } + + public static double doubleReturningMethod() { + return 0; + } + + public static double[] doubleArrayReturningMethod() { + return new double[0]; + } + + public static short shortReturningMethod() { + return 0; + } + + public static short[] shortArrayReturningMethod() { + return new short[0]; + } + + public static void voidReturningMethod() { + } +} diff --git a/robolectric-sandbox/src/test/java/org/robolectric/testing/AClassToRemember.java b/robolectric-sandbox/src/test/java/org/robolectric/testing/AClassToRemember.java new file mode 100644 index 000000000..7761b7cba --- /dev/null +++ b/robolectric-sandbox/src/test/java/org/robolectric/testing/AClassToRemember.java @@ -0,0 +1,4 @@ +package org.robolectric.testing; + +public class AClassToRemember { +} diff --git a/robolectric-sandbox/src/test/java/org/robolectric/testing/AClassWithEqualsHashCodeToString.java b/robolectric-sandbox/src/test/java/org/robolectric/testing/AClassWithEqualsHashCodeToString.java new file mode 100644 index 000000000..8e9167dc5 --- /dev/null +++ b/robolectric-sandbox/src/test/java/org/robolectric/testing/AClassWithEqualsHashCodeToString.java @@ -0,0 +1,22 @@ +package org.robolectric.testing; + +import org.robolectric.annotation.internal.Instrument; + +@Instrument +public class AClassWithEqualsHashCodeToString { + @SuppressWarnings("EqualsWhichDoesntCheckParameterClass") + @Override + public boolean equals(Object obj) { + return true; + } + + @Override + public int hashCode() { + return 42; + } + + @Override + public String toString() { + return "baaaaaah"; + } +} diff --git a/robolectric-sandbox/src/test/java/org/robolectric/testing/AClassWithFinalEqualsHashCode.java b/robolectric-sandbox/src/test/java/org/robolectric/testing/AClassWithFinalEqualsHashCode.java new file mode 100644 index 000000000..e99fccee1 --- /dev/null +++ b/robolectric-sandbox/src/test/java/org/robolectric/testing/AClassWithFinalEqualsHashCode.java @@ -0,0 +1,13 @@ +package org.robolectric.testing; + +public class AClassWithFinalEqualsHashCode { + @Override + public final int hashCode() { + return super.hashCode(); + } + + @Override + public final boolean equals(Object obj) { + return super.equals(obj); + } +} diff --git a/robolectric-sandbox/src/test/java/org/robolectric/testing/AClassWithFunnyConstructors.java b/robolectric-sandbox/src/test/java/org/robolectric/testing/AClassWithFunnyConstructors.java new file mode 100644 index 000000000..7f4dfeb51 --- /dev/null +++ b/robolectric-sandbox/src/test/java/org/robolectric/testing/AClassWithFunnyConstructors.java @@ -0,0 +1,19 @@ +package org.robolectric.testing; + +import org.robolectric.annotation.internal.Instrument; + +@SuppressWarnings("UnusedDeclaration") +@Instrument +public class AClassWithFunnyConstructors { + private final AnUninstrumentedParent uninstrumentedParent; + private String name; + + public AClassWithFunnyConstructors(String name) { + this(new AnUninstrumentedParent(name), "foo"); + this.name = name; + } + + public AClassWithFunnyConstructors(AnUninstrumentedParent uninstrumentedParent, String fooString) { + this.uninstrumentedParent = uninstrumentedParent; + } +} diff --git a/robolectric-sandbox/src/test/java/org/robolectric/testing/AClassWithMethodReturningArray.java b/robolectric-sandbox/src/test/java/org/robolectric/testing/AClassWithMethodReturningArray.java new file mode 100644 index 000000000..40c6becd4 --- /dev/null +++ b/robolectric-sandbox/src/test/java/org/robolectric/testing/AClassWithMethodReturningArray.java @@ -0,0 +1,11 @@ +package org.robolectric.testing; + +import org.robolectric.annotation.internal.Instrument; + +@SuppressWarnings("UnusedDeclaration") +@Instrument +public class AClassWithMethodReturningArray { + public String[] normalMethodReturningArray() { + return new String[] { "hello, working!" }; + } +} diff --git a/robolectric-sandbox/src/test/java/org/robolectric/testing/AClassWithMethodReturningBoolean.java b/robolectric-sandbox/src/test/java/org/robolectric/testing/AClassWithMethodReturningBoolean.java new file mode 100644 index 000000000..c2fe756d0 --- /dev/null +++ b/robolectric-sandbox/src/test/java/org/robolectric/testing/AClassWithMethodReturningBoolean.java @@ -0,0 +1,11 @@ +package org.robolectric.testing; + +import org.robolectric.annotation.internal.Instrument; + +@SuppressWarnings("UnusedDeclaration") +@Instrument +public class AClassWithMethodReturningBoolean { + public boolean normalMethodReturningBoolean(boolean boolArg, boolean[] boolArrayArg) { + return true; + } +} diff --git a/robolectric-sandbox/src/test/java/org/robolectric/testing/AClassWithMethodReturningDouble.java b/robolectric-sandbox/src/test/java/org/robolectric/testing/AClassWithMethodReturningDouble.java new file mode 100644 index 000000000..734d6bb79 --- /dev/null +++ b/robolectric-sandbox/src/test/java/org/robolectric/testing/AClassWithMethodReturningDouble.java @@ -0,0 +1,11 @@ +package org.robolectric.testing; + +import org.robolectric.annotation.internal.Instrument; + +@SuppressWarnings("UnusedDeclaration") +@Instrument +public class AClassWithMethodReturningDouble { + public double normalMethodReturningDouble(double doubleArg) { + return doubleArg + 1; + } +} diff --git a/robolectric-sandbox/src/test/java/org/robolectric/testing/AClassWithMethodReturningInteger.java b/robolectric-sandbox/src/test/java/org/robolectric/testing/AClassWithMethodReturningInteger.java new file mode 100644 index 000000000..71b9eac25 --- /dev/null +++ b/robolectric-sandbox/src/test/java/org/robolectric/testing/AClassWithMethodReturningInteger.java @@ -0,0 +1,11 @@ +package org.robolectric.testing; + +import org.robolectric.annotation.internal.Instrument; + +@SuppressWarnings("UnusedDeclaration") +@Instrument +public class AClassWithMethodReturningInteger { + public int normalMethodReturningInteger(int intArg) { + return intArg + 1; + } +} diff --git a/robolectric-sandbox/src/test/java/org/robolectric/testing/AClassWithNativeMethod.java b/robolectric-sandbox/src/test/java/org/robolectric/testing/AClassWithNativeMethod.java new file mode 100644 index 000000000..be217a657 --- /dev/null +++ b/robolectric-sandbox/src/test/java/org/robolectric/testing/AClassWithNativeMethod.java @@ -0,0 +1,9 @@ +package org.robolectric.testing; + +import org.robolectric.annotation.internal.Instrument; + +@SuppressWarnings("UnusedDeclaration") +@Instrument +public class AClassWithNativeMethod { + public native String nativeMethod(String stringArg, int intArg); +} diff --git a/robolectric-sandbox/src/test/java/org/robolectric/testing/AClassWithNativeMethodReturningPrimitive.java b/robolectric-sandbox/src/test/java/org/robolectric/testing/AClassWithNativeMethodReturningPrimitive.java new file mode 100644 index 000000000..41b75b5bb --- /dev/null +++ b/robolectric-sandbox/src/test/java/org/robolectric/testing/AClassWithNativeMethodReturningPrimitive.java @@ -0,0 +1,9 @@ +package org.robolectric.testing; + +import org.robolectric.annotation.internal.Instrument; + +@SuppressWarnings("UnusedDeclaration") +@Instrument +public class AClassWithNativeMethodReturningPrimitive { + public native int nativeMethod(); +} diff --git a/robolectric-sandbox/src/test/java/org/robolectric/testing/AClassWithNoDefaultConstructor.java b/robolectric-sandbox/src/test/java/org/robolectric/testing/AClassWithNoDefaultConstructor.java new file mode 100644 index 000000000..d191452df --- /dev/null +++ b/robolectric-sandbox/src/test/java/org/robolectric/testing/AClassWithNoDefaultConstructor.java @@ -0,0 +1,12 @@ +package org.robolectric.testing; + +import org.robolectric.annotation.internal.Instrument; + +@SuppressWarnings("UnusedDeclaration") +@Instrument public class AClassWithNoDefaultConstructor { + private String name; + + AClassWithNoDefaultConstructor(String name) { + this.name = name; + } +} diff --git a/robolectric-sandbox/src/test/java/org/robolectric/testing/AClassWithStaticMethod.java b/robolectric-sandbox/src/test/java/org/robolectric/testing/AClassWithStaticMethod.java new file mode 100644 index 000000000..0414418a1 --- /dev/null +++ b/robolectric-sandbox/src/test/java/org/robolectric/testing/AClassWithStaticMethod.java @@ -0,0 +1,11 @@ +package org.robolectric.testing; + +import org.robolectric.annotation.internal.Instrument; + +@SuppressWarnings("UnusedDeclaration") +@Instrument +public class AClassWithStaticMethod { + public static String staticMethod(String stringArg) { + return "staticMethod(" + stringArg + ")"; + } +} diff --git a/robolectric-sandbox/src/test/java/org/robolectric/testing/AClassWithoutEqualsHashCodeToString.java b/robolectric-sandbox/src/test/java/org/robolectric/testing/AClassWithoutEqualsHashCodeToString.java new file mode 100644 index 000000000..ac194e8c2 --- /dev/null +++ b/robolectric-sandbox/src/test/java/org/robolectric/testing/AClassWithoutEqualsHashCodeToString.java @@ -0,0 +1,7 @@ +package org.robolectric.testing; + +import org.robolectric.annotation.internal.Instrument; + +@Instrument +public class AClassWithoutEqualsHashCodeToString { +} diff --git a/robolectric-sandbox/src/test/java/org/robolectric/testing/AFinalClass.java b/robolectric-sandbox/src/test/java/org/robolectric/testing/AFinalClass.java new file mode 100644 index 000000000..c4ce30b2c --- /dev/null +++ b/robolectric-sandbox/src/test/java/org/robolectric/testing/AFinalClass.java @@ -0,0 +1,7 @@ +package org.robolectric.testing; + +import org.robolectric.annotation.internal.Instrument; + +@Instrument +public final class AFinalClass { +} diff --git a/robolectric-sandbox/src/test/java/org/robolectric/testing/AGrandparent.java b/robolectric-sandbox/src/test/java/org/robolectric/testing/AGrandparent.java new file mode 100644 index 000000000..aef9ae1df --- /dev/null +++ b/robolectric-sandbox/src/test/java/org/robolectric/testing/AGrandparent.java @@ -0,0 +1,7 @@ +package org.robolectric.testing; + +import org.robolectric.annotation.internal.Instrument; + +@Instrument +public class AGrandparent { +} diff --git a/robolectric-sandbox/src/test/java/org/robolectric/testing/AParent.java b/robolectric-sandbox/src/test/java/org/robolectric/testing/AParent.java new file mode 100644 index 000000000..b640e4f4a --- /dev/null +++ b/robolectric-sandbox/src/test/java/org/robolectric/testing/AParent.java @@ -0,0 +1,7 @@ +package org.robolectric.testing; + +import org.robolectric.annotation.internal.Instrument; + +@Instrument +public class AParent extends AGrandparent { +} diff --git a/robolectric-sandbox/src/test/java/org/robolectric/testing/AnEnum.java b/robolectric-sandbox/src/test/java/org/robolectric/testing/AnEnum.java new file mode 100644 index 000000000..f063061c7 --- /dev/null +++ b/robolectric-sandbox/src/test/java/org/robolectric/testing/AnEnum.java @@ -0,0 +1,8 @@ +package org.robolectric.testing; + +import org.robolectric.annotation.internal.Instrument; + +@Instrument +public enum AnEnum { + ONE, TWO, MANY +} diff --git a/robolectric-sandbox/src/test/java/org/robolectric/testing/AnExampleClass.java b/robolectric-sandbox/src/test/java/org/robolectric/testing/AnExampleClass.java new file mode 100644 index 000000000..2e6c07a94 --- /dev/null +++ b/robolectric-sandbox/src/test/java/org/robolectric/testing/AnExampleClass.java @@ -0,0 +1,15 @@ +package org.robolectric.testing; + +import org.robolectric.annotation.internal.Instrument; + +@SuppressWarnings("UnusedDeclaration") +@Instrument +public class AnExampleClass { + static int foo = 123; + + public String normalMethod(String stringArg, int intArg) { + return "normalMethod(" + stringArg + ", " + intArg + ")"; + } + + // abstract void abstractMethod(); todo +} diff --git a/robolectric-sandbox/src/test/java/org/robolectric/testing/AnInstrumentedChild.java b/robolectric-sandbox/src/test/java/org/robolectric/testing/AnInstrumentedChild.java new file mode 100644 index 000000000..7357a3e72 --- /dev/null +++ b/robolectric-sandbox/src/test/java/org/robolectric/testing/AnInstrumentedChild.java @@ -0,0 +1,13 @@ +package org.robolectric.testing; + +import org.robolectric.annotation.internal.Instrument; + +@Instrument +public class AnInstrumentedChild extends AnUninstrumentedParent { + public final String childName; + + public AnInstrumentedChild(String name) { + super(name.toUpperCase() + "'s child"); + this.childName = name; + } +} diff --git a/robolectric-sandbox/src/test/java/org/robolectric/testing/AnInstrumentedClassWithoutToStringWithSuperToString.java b/robolectric-sandbox/src/test/java/org/robolectric/testing/AnInstrumentedClassWithoutToStringWithSuperToString.java new file mode 100644 index 000000000..4ad06f6e2 --- /dev/null +++ b/robolectric-sandbox/src/test/java/org/robolectric/testing/AnInstrumentedClassWithoutToStringWithSuperToString.java @@ -0,0 +1,7 @@ +package org.robolectric.testing; + +import org.robolectric.annotation.internal.Instrument; + +@Instrument +public class AnInstrumentedClassWithoutToStringWithSuperToString extends AnUninstrumentedClassWithToString { +} diff --git a/robolectric-sandbox/src/test/java/org/robolectric/testing/AnUninstrumentedClass.java b/robolectric-sandbox/src/test/java/org/robolectric/testing/AnUninstrumentedClass.java new file mode 100644 index 000000000..58771f034 --- /dev/null +++ b/robolectric-sandbox/src/test/java/org/robolectric/testing/AnUninstrumentedClass.java @@ -0,0 +1,4 @@ +package org.robolectric.testing; + +public class AnUninstrumentedClass { +} diff --git a/robolectric-sandbox/src/test/java/org/robolectric/testing/AnUninstrumentedClassWithToString.java b/robolectric-sandbox/src/test/java/org/robolectric/testing/AnUninstrumentedClassWithToString.java new file mode 100644 index 000000000..f5bcefe35 --- /dev/null +++ b/robolectric-sandbox/src/test/java/org/robolectric/testing/AnUninstrumentedClassWithToString.java @@ -0,0 +1,8 @@ +package org.robolectric.testing; + +public class AnUninstrumentedClassWithToString { + @Override + public String toString() { + return "baaaaaah"; + } +} diff --git a/robolectric-sandbox/src/test/java/org/robolectric/testing/AnUninstrumentedParent.java b/robolectric-sandbox/src/test/java/org/robolectric/testing/AnUninstrumentedParent.java new file mode 100644 index 000000000..6737266ae --- /dev/null +++ b/robolectric-sandbox/src/test/java/org/robolectric/testing/AnUninstrumentedParent.java @@ -0,0 +1,17 @@ +package org.robolectric.testing; + +import org.robolectric.annotation.internal.DoNotInstrument; + +@DoNotInstrument +public class AnUninstrumentedParent { + public final String parentName; + + public AnUninstrumentedParent(String name) { + this.parentName = name; + } + + @Override + public String toString() { + return "UninstrumentedParent{parentName='" + parentName + '\'' + '}'; + } +} diff --git a/robolectric-sandbox/src/test/java/org/robolectric/testing/Foo.java b/robolectric-sandbox/src/test/java/org/robolectric/testing/Foo.java new file mode 100644 index 000000000..6034ec27a --- /dev/null +++ b/robolectric-sandbox/src/test/java/org/robolectric/testing/Foo.java @@ -0,0 +1,18 @@ +package org.robolectric.testing; + +import org.robolectric.annotation.internal.Instrument; + +@Instrument +public class Foo { + public Foo(String s) { + throw new RuntimeException("stub!"); + } + + public String getName() { + throw new RuntimeException("stub!"); + } + + public void findFooById(int i) { + throw new RuntimeException("stub!"); + } +} diff --git a/robolectric-sandbox/src/test/java/org/robolectric/testing/Pony.java b/robolectric-sandbox/src/test/java/org/robolectric/testing/Pony.java new file mode 100644 index 000000000..eb8ee85e9 --- /dev/null +++ b/robolectric-sandbox/src/test/java/org/robolectric/testing/Pony.java @@ -0,0 +1,36 @@ +package org.robolectric.testing; + +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; +import org.robolectric.annotation.internal.Instrument; + +@Instrument +public class Pony { + public Pony() { + } + + public String ride(String where) { + return "Whinny! You're on my " + where + "!"; + } + + public static String prance(String where) { + return "I'm prancing to " + where + "!"; + } + + public String saunter(String where) { + return "Off I saunter to " + where + "!"; + } + + @Implements(Pony.class) + public static class ShadowPony { + @Implementation + public String ride(String where) { + return "Fake whinny! You're on my " + where + "!"; + } + + @Implementation + public static String prance(String where) { + return "I'm shadily prancing to " + where + "!"; + } + } +} diff --git a/robolectric-sandbox/src/test/java/org/robolectric/testing/ShadowFoo.java b/robolectric-sandbox/src/test/java/org/robolectric/testing/ShadowFoo.java new file mode 100644 index 000000000..2c6334fb8 --- /dev/null +++ b/robolectric-sandbox/src/test/java/org/robolectric/testing/ShadowFoo.java @@ -0,0 +1,25 @@ +package org.robolectric.testing; + +import org.robolectric.ShadowWranglerIntegrationTest; +import org.robolectric.annotation.Implements; +import org.robolectric.annotation.RealObject; + +@Implements(Foo.class) +public class ShadowFoo extends ShadowWranglerIntegrationTest.ShadowFooParent { + @RealObject public Foo realFooField; + public Foo realFooInConstructor; + public String name; + + @Override + @SuppressWarnings({"UnusedDeclaration"}) + public void __constructor__(String name) { + super.__constructor__(name); + this.name = name; + realFooInConstructor = realFooField; + } + + @SuppressWarnings({"UnusedDeclaration"}) + public String getName() { + return name; + } +} |