blob: 016e883c76cfe45626a4293c1b27e080a2fda0d3 [file] [log] [blame]
package org.robolectric.internal.bytecode;
import static com.google.common.truth.Truth.assertThat;
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.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.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.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;
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.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.annotation.Nonnull;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.Mockito;
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.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.AFinalClass;
import org.robolectric.testing.AnEnum;
import org.robolectric.testing.AnExampleClass;
import org.robolectric.testing.AnInstrumentedChild;
import org.robolectric.testing.AnUninstrumentedClass;
import org.robolectric.testing.AnUninstrumentedParent;
import org.robolectric.util.ReflectionHelpers;
import org.robolectric.util.Util;
@RunWith(JUnit4.class)
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((Object) 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(ClassDetails.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();
// Java 9 doesn't allow updates to final fields from outside <init> or <clinit>:
// https://bugs.openjdk.java.net/browse/JDK-8157181
// Therefore, these fields need to be nonfinal / be made nonfinal.
assertThat(Modifier.isFinal(roboDataField.getModifiers())).isFalse();
assertThat(Modifier.isFinal(exampleClass.getField("STATIC_FINAL_FIELD").getModifiers()))
.isFalse();
assertThat(Modifier.isFinal(exampleClass.getField("nonstaticFinalField").getModifiers()))
.isFalse();
}
@Test
public void callingNormalMethodShouldInvokeClassHandler() throws Exception {
Class<?> exampleClass = loadClass(AnExampleClass.class);
Method normalMethod = exampleClass.getMethod("normalMethod", String.class, int.class);
Object exampleInstance = exampleClass.getDeclaredConstructor().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(exampleClass.getName(), "normalMethod");
Method directMethod = exampleClass.getDeclaredMethod(methodName, String.class, int.class);
directMethod.setAccessible(true);
Object exampleInstance = exampleClass.getDeclaredConstructor().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(exampleClass.getName(), "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(exampleClass.getName(), "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.getDeclaredConstructor().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.getDeclaredConstructor().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.getDeclaredConstructor().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.getDeclaredConstructor().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.getDeclaredConstructor().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.getDeclaredConstructor().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.getDeclaredConstructor().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).getDeclaredConstructor().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(declaringClass.getName(), methodName);
Method directMethod = declaringClass.getDeclaredMethod(directMethodName, argClasses);
directMethod.setAccessible(true);
return directMethod;
}
@Test
public void shouldNotInstrumentFinalEqualsHashcode() throws ClassNotFoundException {
loadClass(AClassThatExtendsAClassWithFinalEqualsHashCode.class);
}
@Test
@SuppressWarnings("CheckReturnValue")
public void shouldAlsoInstrumentEqualsAndHashCodeAndToStringWhenDeclared() throws Exception {
Class<?> theClass = loadClass(AClassWithEqualsHashCodeToString.class);
Object instance = theClass.getDeclaredConstructor().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 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.getDeclaredConstructor().newInstance();
Method method =
theClass.getDeclaredMethod(
shadow.directMethodName(theClass.getName(), 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.getDeclaredConstructor().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.getDeclaredConstructor().newInstance();
Object output =
theClass
.getMethod("interactWithForgettableStaticMethod")
.invoke(shadow.directlyOn(instance, (Class<Object>) theClass));
assertEquals("yess? forget this: null", output);
}
@Nonnull
private InstrumentationConfiguration.Builder configureBuilder() {
InstrumentationConfiguration.Builder builder = InstrumentationConfiguration.newBuilder();
builder
.doNotAcquirePackage("java.")
.doNotAcquirePackage("jdk.internal.")
.doNotAcquirePackage("sun.")
.doNotAcquirePackage("com.sun.")
.doNotAcquirePackage("org.robolectric.internal.")
.doNotAcquirePackage("org.robolectric.pluginapi.");
return builder;
}
@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.getDeclaredConstructor().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 final 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 MethodHandle getShadowCreator(Class<?> theClass) {
return dropArguments(constant(String.class, "a shadow!"), 0, theClass);
}
@SuppressWarnings(value = {"UnusedDeclaration", "unused"})
private Object invoke(InvocationProfile invocationProfile, Object instance, Object[] params) {
return methodInvoked(
invocationProfile.clazz,
invocationProfile.methodName,
instance,
invocationProfile.paramTypes,
params);
}
@Override
public MethodHandle findShadowMethodHandle(
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());
}
@Test
public void shouldDumpClassesWhenConfigured() throws Exception {
Path tempDir = Files.createTempDirectory("SandboxClassLoaderTest");
System.setProperty("robolectric.dumpClassesDirectory", tempDir.toAbsolutePath().toString());
ClassLoader classLoader = new SandboxClassLoader(configureBuilder().build());
classLoader.loadClass(AnExampleClass.class.getName());
try (Stream<Path> stream = Files.list(tempDir)) {
List<Path> files = stream.collect(Collectors.toList());
assertThat(files).hasSize(1);
assertThat(files.get(0).toAbsolutePath().toString())
.containsMatch("org.robolectric.testing.AnExampleClass-robo-instrumented-\\d+.class");
Files.delete(files.get(0));
} finally {
Files.delete(tempDir);
System.clearProperty("robolectric.dumpClassesDirectory");
}
}
}