package org.robolectric;
import javax.annotation.Nonnull;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import org.robolectric.internal.bytecode.ClassHandler;
import org.robolectric.internal.bytecode.ClassInfo;
import org.robolectric.internal.bytecode.InstrumentationConfiguration;
import org.robolectric.internal.bytecode.Interceptor;
import org.robolectric.internal.bytecode.Interceptors;
import org.robolectric.internal.bytecode.InvocationProfile;
import org.robolectric.internal.bytecode.InvokeDynamic;
import org.robolectric.internal.bytecode.InvokeDynamicSupport;
import org.robolectric.internal.bytecode.MethodRef;
import org.robolectric.internal.bytecode.RobolectricInternals;
import org.robolectric.internal.bytecode.SandboxClassLoader;
import org.robolectric.internal.bytecode.ShadowConstants;
import org.robolectric.internal.bytecode.ShadowImpl;
import org.robolectric.internal.bytecode.ShadowInvalidator;
import org.robolectric.shadow.api.Shadow;
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.constant;
import static java.lang.invoke.MethodHandles.dropArguments;
import static java.lang.invoke.MethodHandles.insertArguments;
import static java.lang.invoke.MethodType.methodType;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
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 SandboxClassLoaderTest {
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(shadow.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 SandboxClassLoader(config);
Class<?> exampleClass = classLoader.loadClass(AnExampleClass.class.getName());
assertSame(getClass().getClassLoader(), exampleClass.getClassLoader());
}
@Test
public void shouldPerformClassLoadForAcquiredClasses() throws Exception {
ClassLoader classLoader = new SandboxClassLoader(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 SandboxClassLoader(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 SandboxClassLoader(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 SandboxClassLoader(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 SandboxClassLoader(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 SandboxClassLoader(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 SandboxClassLoader(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 SandboxClassLoader(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));
}
@Nonnull
private InstrumentationConfiguration.Builder configureBuilder() {
InstrumentationConfiguration.Builder builder = InstrumentationConfiguration.newBuilder();
builder.doNotAcquirePackage("java.")
.doNotAcquirePackage("sun.")
.doNotAcquirePackage("com.sun.")
.doNotAcquirePackage("org.robolectric.internal.")
;
return builder;
}
@Test
public void shouldPassArgumentsFromInterceptedMethods() throws Exception {
if (InvokeDynamic.ENABLED) return;
classHandler.valueToReturnFromIntercept = 10L;
setClassLoader(new SandboxClassLoader(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/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 SandboxClassLoader(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[]{}));
}
/////////////////////////////
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 SandboxClassLoader(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());
}
}