aboutsummaryrefslogtreecommitdiff
path: root/robolectric-sandbox
diff options
context:
space:
mode:
authorChristian Williams <christianw@google.com>2017-01-25 13:52:23 -0800
committerChristian Williams <christianw@google.com>2017-02-07 15:07:55 -0800
commit58f07eef06aee72314f27ed2a24fa745243f83ef (patch)
treebd37fed4ca6b5bbaf2854193bf77a12d32a57914 /robolectric-sandbox
parent7980652753a7e4af50f935cad531a886be348f8a (diff)
downloadrobolectric-shadows-58f07eef06aee72314f27ed2a24fa745243f83ef.tar.gz
Rename robolectric-instrumentation to robolectric-sandbox.
Diffstat (limited to 'robolectric-sandbox')
-rw-r--r--robolectric-sandbox/build.gradle23
-rw-r--r--robolectric-sandbox/src/main/java/org/robolectric/internal/DirectObjectMarker.java9
-rw-r--r--robolectric-sandbox/src/main/java/org/robolectric/internal/InvokeDynamic.java23
-rw-r--r--robolectric-sandbox/src/main/java/org/robolectric/internal/ProxyMaker.java117
-rw-r--r--robolectric-sandbox/src/main/java/org/robolectric/internal/ShadowConstants.java9
-rw-r--r--robolectric-sandbox/src/main/java/org/robolectric/internal/ShadowImpl.java68
-rw-r--r--robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/ClassHandler.java28
-rw-r--r--robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/ClassInfo.java38
-rw-r--r--robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/InstrumentationConfiguration.java233
-rw-r--r--robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/InstrumentingClassLoader.java1332
-rw-r--r--robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/Interceptor.java34
-rw-r--r--robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/Interceptors.java47
-rw-r--r--robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/InvocationProfile.java70
-rw-r--r--robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/InvokeDynamicSupport.java165
-rw-r--r--robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/MethodCallSite.java50
-rw-r--r--robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/MethodRef.java42
-rw-r--r--robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/MethodSignature.java42
-rw-r--r--robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/RoboCallSite.java17
-rw-r--r--robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/RoboConfig.java31
-rw-r--r--robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/RoboType.java29
-rw-r--r--robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/RobolectricInternals.java67
-rw-r--r--robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/Sandbox.java68
-rw-r--r--robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/ShadowConfig.java62
-rw-r--r--robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/ShadowInvalidator.java43
-rw-r--r--robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/ShadowMap.java197
-rw-r--r--robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/ShadowWrangler.java493
-rw-r--r--robolectric-sandbox/src/main/java/org/robolectric/util/Function.java8
-rw-r--r--robolectric-sandbox/src/main/java/org/robolectric/util/JavaVersion.java31
-rw-r--r--robolectric-sandbox/src/test/java/org/robolectric/ClassicSuperHandlingTest.java83
-rw-r--r--robolectric-sandbox/src/test/java/org/robolectric/InstrumentingClassLoaderTest.java815
-rw-r--r--robolectric-sandbox/src/test/java/org/robolectric/JavaVersionTest.java48
-rw-r--r--robolectric-sandbox/src/test/java/org/robolectric/RealApisTest.java44
-rw-r--r--robolectric-sandbox/src/test/java/org/robolectric/RobolectricInternalsTest.java154
-rw-r--r--robolectric-sandbox/src/test/java/org/robolectric/ShadowWranglerIntegrationTest.java325
-rw-r--r--robolectric-sandbox/src/test/java/org/robolectric/ShadowingTest.java248
-rw-r--r--robolectric-sandbox/src/test/java/org/robolectric/StaticInitializerTest.java68
-rw-r--r--robolectric-sandbox/src/test/java/org/robolectric/ThreadSafetyTest.java55
-rw-r--r--robolectric-sandbox/src/test/java/org/robolectric/internal/ProxyMakerTest.java65
-rw-r--r--robolectric-sandbox/src/test/java/org/robolectric/testing/AChild.java7
-rw-r--r--robolectric-sandbox/src/test/java/org/robolectric/testing/AClassThatCallsAMethodReturningAForgettableClass.java15
-rw-r--r--robolectric-sandbox/src/test/java/org/robolectric/testing/AClassThatExtendsAClassWithFinalEqualsHashCode.java7
-rw-r--r--robolectric-sandbox/src/test/java/org/robolectric/testing/AClassThatRefersToAForgettableClass.java20
-rw-r--r--robolectric-sandbox/src/test/java/org/robolectric/testing/AClassThatRefersToAForgettableClassInItsConstructor.java13
-rw-r--r--robolectric-sandbox/src/test/java/org/robolectric/testing/AClassThatRefersToAForgettableClassInMethodCalls.java15
-rw-r--r--robolectric-sandbox/src/test/java/org/robolectric/testing/AClassThatRefersToAForgettableClassInMethodCallsReturningPrimitive.java59
-rw-r--r--robolectric-sandbox/src/test/java/org/robolectric/testing/AClassToForget.java70
-rw-r--r--robolectric-sandbox/src/test/java/org/robolectric/testing/AClassToRemember.java4
-rw-r--r--robolectric-sandbox/src/test/java/org/robolectric/testing/AClassWithEqualsHashCodeToString.java22
-rw-r--r--robolectric-sandbox/src/test/java/org/robolectric/testing/AClassWithFinalEqualsHashCode.java13
-rw-r--r--robolectric-sandbox/src/test/java/org/robolectric/testing/AClassWithFunnyConstructors.java19
-rw-r--r--robolectric-sandbox/src/test/java/org/robolectric/testing/AClassWithMethodReturningArray.java11
-rw-r--r--robolectric-sandbox/src/test/java/org/robolectric/testing/AClassWithMethodReturningBoolean.java11
-rw-r--r--robolectric-sandbox/src/test/java/org/robolectric/testing/AClassWithMethodReturningDouble.java11
-rw-r--r--robolectric-sandbox/src/test/java/org/robolectric/testing/AClassWithMethodReturningInteger.java11
-rw-r--r--robolectric-sandbox/src/test/java/org/robolectric/testing/AClassWithNativeMethod.java9
-rw-r--r--robolectric-sandbox/src/test/java/org/robolectric/testing/AClassWithNativeMethodReturningPrimitive.java9
-rw-r--r--robolectric-sandbox/src/test/java/org/robolectric/testing/AClassWithNoDefaultConstructor.java12
-rw-r--r--robolectric-sandbox/src/test/java/org/robolectric/testing/AClassWithStaticMethod.java11
-rw-r--r--robolectric-sandbox/src/test/java/org/robolectric/testing/AClassWithoutEqualsHashCodeToString.java7
-rw-r--r--robolectric-sandbox/src/test/java/org/robolectric/testing/AFinalClass.java7
-rw-r--r--robolectric-sandbox/src/test/java/org/robolectric/testing/AGrandparent.java7
-rw-r--r--robolectric-sandbox/src/test/java/org/robolectric/testing/AParent.java7
-rw-r--r--robolectric-sandbox/src/test/java/org/robolectric/testing/AnEnum.java8
-rw-r--r--robolectric-sandbox/src/test/java/org/robolectric/testing/AnExampleClass.java15
-rw-r--r--robolectric-sandbox/src/test/java/org/robolectric/testing/AnInstrumentedChild.java13
-rw-r--r--robolectric-sandbox/src/test/java/org/robolectric/testing/AnInstrumentedClassWithoutToStringWithSuperToString.java7
-rw-r--r--robolectric-sandbox/src/test/java/org/robolectric/testing/AnUninstrumentedClass.java4
-rw-r--r--robolectric-sandbox/src/test/java/org/robolectric/testing/AnUninstrumentedClassWithToString.java8
-rw-r--r--robolectric-sandbox/src/test/java/org/robolectric/testing/AnUninstrumentedParent.java17
-rw-r--r--robolectric-sandbox/src/test/java/org/robolectric/testing/Foo.java18
-rw-r--r--robolectric-sandbox/src/test/java/org/robolectric/testing/Pony.java36
-rw-r--r--robolectric-sandbox/src/test/java/org/robolectric/testing/ShadowFoo.java25
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;
+ }
+}