/* * Copyright (C) 2011 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.dx.stock; import android.os.Build; import com.android.dx.DexMakerTest; import junit.framework.AssertionFailedError; import org.junit.After; import org.junit.Before; import org.junit.Test; import java.io.File; import java.io.IOException; import java.lang.reflect.Field; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.UndeclaredThrowableException; import java.util.ArrayList; import java.util.Arrays; import java.util.Map; import java.util.Random; import java.util.concurrent.Callable; import java.util.concurrent.atomic.AtomicInteger; import static com.android.dx.util.TestUtil.DELTA_DOUBLE; import static com.android.dx.util.TestUtil.DELTA_FLOAT; import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertNotNull; import static junit.framework.Assert.assertSame; import static junit.framework.Assert.assertTrue; import static junit.framework.Assert.fail; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotSame; import static org.junit.Assume.assumeTrue; public class ProxyBuilderTest { private FakeInvocationHandler fakeHandler = new FakeInvocationHandler(); private File versionedDxDir = new File(DexMakerTest.getDataDirectory(), "v1"); @Before public void setUp() throws Exception { versionedDxDir.mkdirs(); clearVersionedDxDir(); getGeneratedProxyClasses().clear(); } @After public void tearDown() throws Exception { getGeneratedProxyClasses().clear(); clearVersionedDxDir(); } private void clearVersionedDxDir() { for (File file : versionedDxDir.listFiles()) { file.delete(); } } public static class SimpleClass { public String simpleMethod() { throw new AssertionFailedError(); } } public static class ExampleClass { public String exampleMethod() { throw new AssertionFailedError(); } } public static class ExampleOperationClass { public String exampleMethod() { throw new AssertionFailedError(); } } @Test public void testExampleOperation() throws Throwable { fakeHandler.setFakeResult("expected"); ExampleClass proxy = proxyFor(ExampleClass.class).build(); assertEquals("expected", proxy.exampleMethod()); assertEquals(2, versionedDxDir.listFiles().length); } @Test public void testExampleOperation_DexMakerCaching() throws Throwable { fakeHandler.setFakeResult("expected"); ExampleOperationClass proxy = proxyFor(ExampleOperationClass.class).build(); assertEquals(2, versionedDxDir.listFiles().length); assertEquals("expected", proxy.exampleMethod()); // Force ProxyBuilder to create a DexMaker generator and call DexMaker.generateAndLoad(). getGeneratedProxyClasses().clear(); proxy = proxyFor(ExampleOperationClass.class).build(); assertEquals(2, versionedDxDir.listFiles().length); assertEquals("expected", proxy.exampleMethod()); } public static class ConstructorTakesArguments { private final String argument; public ConstructorTakesArguments(String arg) { argument = arg; } public String method() { throw new AssertionFailedError(); } } @Test public void testConstruction_SucceedsIfCorrectArgumentsProvided() throws Throwable { ConstructorTakesArguments proxy = proxyFor(ConstructorTakesArguments.class) .constructorArgTypes(String.class) .constructorArgValues("hello") .build(); assertEquals("hello", proxy.argument); proxy.method(); } @Test(expected = IllegalArgumentException.class) public void testConstruction_FailsWithWrongNumberOfArguments() throws Throwable { proxyFor(ConstructorTakesArguments.class).build(); } @Test(expected = UnsupportedOperationException.class) public void testClassIsNotAccessbile_FailsWithUnsupportedOperationException() throws Exception { class MethodVisibilityClass {} proxyFor(MethodVisibilityClass.class).build(); } private static class PrivateVisibilityClass {} @Test(expected = UnsupportedOperationException.class) public void testPrivateClass_FailsWithUnsupportedOperationException() throws Exception { proxyFor(PrivateVisibilityClass.class).build(); } protected static class ProtectedVisibilityClass { public String foo() { throw new AssertionFailedError(); } } @Test public void testProtectedVisibility_WorksFine() throws Exception { assertEquals("fake result", proxyFor(ProtectedVisibilityClass.class).build().foo()); } public static class HasFinalMethod { public String nonFinalMethod() { return "non-final method"; } public final String finalMethod() { return "final method"; } } @Test public void testCanProxyClassesWithFinalMethods_WillNotCallTheFinalMethod() throws Throwable { HasFinalMethod proxy = proxyFor(HasFinalMethod.class).build(); assertEquals("final method", proxy.finalMethod()); assertEquals("fake result", proxy.nonFinalMethod()); } public static class HasPrivateMethod { private String result() { return "expected"; } } @Test public void testProxyingPrivateMethods_NotIntercepted() throws Throwable { HasPrivateMethod proxy = proxyFor(HasPrivateMethod.class).build(); try { proxy.getClass().getDeclaredMethod("result"); fail(); } catch (NoSuchMethodException expected) { } assertEquals("expected", proxy.result()); } public static class HasPackagePrivateMethod { String result() { throw new AssertionFailedError(); } } @Test public void testProxyingPackagePrivateMethods_NotIntercepted() throws Throwable { HasPackagePrivateMethod proxy = proxyFor(HasPackagePrivateMethod.class) .build(); try { proxy.getClass().getDeclaredMethod("result"); fail(); } catch (NoSuchMethodException expected) { } try { proxy.result(); } catch (AssertionFailedError expected) { return; } fail(); } public static class HasPackagePrivateMethodSharedClassLoader { String result() { throw new AssertionFailedError(); } } @Test public void testProxyingPackagePrivateMethodsWithSharedClassLoader_AreIntercepted() throws Throwable { assumeTrue(Build.VERSION.SDK_INT >= 24); assertEquals("fake result", proxyFor(HasPackagePrivateMethodSharedClassLoader.class) .withSharedClassLoader().build().result()); } public static class HasProtectedMethod { protected String result() { throw new AssertionFailedError(); } } @Test public void testProxyingProtectedMethods_AreIntercepted() throws Throwable { assertEquals("fake result", proxyFor(HasProtectedMethod.class).build().result()); } public static class MyParentClass { String someMethod() { return "package"; } } public static class MyChildClassWithProtectedMethod extends MyParentClass { @Override protected String someMethod() { return "protected"; } } public static class MyChildClassWithPublicMethod extends MyParentClass { @Override public String someMethod() { return "public"; } } @Test public void testProxying_ClassHierarchy() throws Throwable { assertEquals("package", proxyFor(MyParentClass.class).build().someMethod()); assertEquals("fake result", proxyFor(MyChildClassWithProtectedMethod.class).build().someMethod()); assertEquals("fake result", proxyFor(MyChildClassWithPublicMethod.class).build().someMethod()); } public static class HasVoidMethod { public void dangerousMethod() { fail(); } } @Test public void testVoidMethod_ShouldNotThrowRuntimeException() throws Throwable { proxyFor(HasVoidMethod.class).build().dangerousMethod(); } @Test @SuppressWarnings({"EqualsWithItself", "SelfEquals"}) public void testObjectMethodsAreAlsoProxied() throws Throwable { Object proxy = proxyFor(Object.class).build(); fakeHandler.setFakeResult("mystring"); assertEquals("mystring", proxy.toString()); fakeHandler.setFakeResult(-1); assertEquals(-1, proxy.hashCode()); fakeHandler.setFakeResult(false); assertEquals(false, proxy.equals(proxy)); } public static class AllReturnTypes { public boolean getBoolean() { return true; } public int getInt() { return 1; } public byte getByte() { return 2; } public long getLong() { return 3L; } public short getShort() { return 4; } public float getFloat() { return 5f; } public double getDouble() { return 6.0; } public char getChar() { return 'c'; } public int[] getIntArray() { return new int[] { 8, 9 }; } public String[] getStringArray() { return new String[] { "d", "e" }; } } @Test public void testAllReturnTypes() throws Throwable { AllReturnTypes proxy = proxyFor(AllReturnTypes.class).build(); fakeHandler.setFakeResult(false); assertEquals(false, proxy.getBoolean()); fakeHandler.setFakeResult(8); assertEquals(8, proxy.getInt()); fakeHandler.setFakeResult((byte) 9); assertEquals(9, proxy.getByte()); fakeHandler.setFakeResult(10L); assertEquals(10, proxy.getLong()); fakeHandler.setFakeResult((short) 11); assertEquals(11, proxy.getShort()); fakeHandler.setFakeResult(12f); assertEquals(12f, proxy.getFloat(), DELTA_FLOAT); fakeHandler.setFakeResult(13.0); assertEquals(13.0, proxy.getDouble(), DELTA_FLOAT); fakeHandler.setFakeResult('z'); assertEquals('z', proxy.getChar()); fakeHandler.setFakeResult(new int[] { -1, -2 }); assertEquals("[-1, -2]", Arrays.toString(proxy.getIntArray())); fakeHandler.setFakeResult(new String[] { "x", "y" }); assertEquals("[x, y]", Arrays.toString(proxy.getStringArray())); } public static class PassThroughAllTypes { public boolean getBoolean(boolean input) { return input; } public int getInt(int input) { return input; } public byte getByte(byte input) { return input; } public long getLong(long input) { return input; } public short getShort(short input) { return input; } public float getFloat(float input) { return input; } public double getDouble(double input) { return input; } public char getChar(char input) { return input; } public String getString(String input) { return input; } public Object getObject(Object input) { return input; } public void getNothing() {} } public static class InvokeSuperHandler implements InvocationHandler { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { return ProxyBuilder.callSuper(proxy, method, args); } } @Test public void testPassThroughWorksForAllTypes() throws Exception { PassThroughAllTypes proxy = proxyFor(PassThroughAllTypes.class) .handler(new InvokeSuperHandler()) .build(); assertEquals(false, proxy.getBoolean(false)); assertEquals(true, proxy.getBoolean(true)); assertEquals(0, proxy.getInt(0)); assertEquals(1, proxy.getInt(1)); assertEquals((byte) 2, proxy.getByte((byte) 2)); assertEquals((byte) 3, proxy.getByte((byte) 3)); assertEquals(4L, proxy.getLong(4L)); assertEquals(5L, proxy.getLong(5L)); assertEquals((short) 6, proxy.getShort((short) 6)); assertEquals((short) 7, proxy.getShort((short) 7)); assertEquals(8f, proxy.getFloat(8f), DELTA_FLOAT); assertEquals(9f, proxy.getFloat(9f), DELTA_FLOAT); assertEquals(10.0, proxy.getDouble(10.0), DELTA_DOUBLE); assertEquals(11.0, proxy.getDouble(11.0), DELTA_DOUBLE); assertEquals('a', proxy.getChar('a')); assertEquals('b', proxy.getChar('b')); assertEquals("asdf", proxy.getString("asdf")); assertEquals("qwer", proxy.getString("qwer")); assertEquals(null, proxy.getString(null)); Object a = new Object(); assertEquals(a, proxy.getObject(a)); assertEquals(null, proxy.getObject(null)); proxy.getNothing(); } public static class ExtendsAllReturnTypes extends AllReturnTypes { public int example() { return 0; } } @Test public void testProxyWorksForSuperclassMethodsAlso() throws Throwable { ExtendsAllReturnTypes proxy = proxyFor(ExtendsAllReturnTypes.class).build(); fakeHandler.setFakeResult(99); assertEquals(99, proxy.example()); assertEquals(99, proxy.getInt()); assertEquals(99, proxy.hashCode()); } public static class HasOddParams { public long method(int first, Integer second) { throw new AssertionFailedError(); } } @Test public void testMixingBoxedAndUnboxedParams() throws Throwable { HasOddParams proxy = proxyFor(HasOddParams.class).build(); fakeHandler.setFakeResult(99L); assertEquals(99L, proxy.method(1, Integer.valueOf(2))); } public static class SingleInt { public String getString(int value) { throw new AssertionFailedError(); } } @Test public void testSinglePrimitiveParameter() throws Throwable { InvocationHandler handler = new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { return "asdf" + ((Integer) args[0]).intValue(); } }; assertEquals("asdf1", proxyFor(SingleInt.class).handler(handler).build().getString(1)); } public static class TwoConstructors { private final String string; public TwoConstructors() { string = "no-arg"; } public TwoConstructors(boolean unused) { string = "one-arg"; } } @Test public void testNoConstructorArguments_CallsNoArgConstructor() throws Throwable { TwoConstructors twoConstructors = proxyFor(TwoConstructors.class).build(); assertEquals("no-arg", twoConstructors.string); } @Test public void testWithoutInvocationHandler_ThrowsIllegalArgumentException() throws Throwable { try { ProxyBuilder.forClass(TwoConstructors.class) .dexCache(DexMakerTest.getDataDirectory()) .build(); fail(); } catch (IllegalArgumentException expected) {} } public static class HardToConstructCorrectly { public HardToConstructCorrectly() { fail(); } public HardToConstructCorrectly(Runnable ignored) { fail(); } public HardToConstructCorrectly(Exception ignored) { fail(); } public HardToConstructCorrectly(Boolean ignored) { /* safe */ } public HardToConstructCorrectly(Integer ignored) { fail(); } } @Test public void testHardToConstruct_WorksIfYouSpecifyTheConstructorCorrectly() throws Throwable { proxyFor(HardToConstructCorrectly.class) .constructorArgTypes(Boolean.class) .constructorArgValues(true) .build(); } @Test public void testHardToConstruct_EvenWorksWhenArgsAreAmbiguous() throws Throwable { proxyFor(HardToConstructCorrectly.class) .constructorArgTypes(Boolean.class) .constructorArgValues(new Object[] { null }) .build(); } @Test(expected = IllegalArgumentException.class) public void testHardToConstruct_DoesNotInferTypesFromValues() throws Throwable { proxyFor(HardToConstructCorrectly.class) .constructorArgValues(true) .build(); } @Test public void testDefaultProxyHasSuperMethodToAccessOriginal() throws Exception { Object objectProxy = proxyFor(Object.class).build(); assertNotNull(objectProxy.getClass().getMethod("super$hashCode$int")); } public static class PrintsOddAndValue { public String method(int value) { return "odd " + value; } } @Test public void testSometimesDelegateToSuper() throws Exception { InvocationHandler delegatesOddValues = new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (method.getName().equals("method")) { int intValue = ((Integer) args[0]).intValue(); if (intValue % 2 == 0) { return "even " + intValue; } } return ProxyBuilder.callSuper(proxy, method, args); } }; PrintsOddAndValue proxy = proxyFor(PrintsOddAndValue.class) .handler(delegatesOddValues) .build(); assertEquals("even 0", proxy.method(0)); assertEquals("odd 1", proxy.method(1)); assertEquals("even 2", proxy.method(2)); assertEquals("odd 3", proxy.method(3)); } @Test public void testCallSuperThrows() throws Exception { InvocationHandler handler = new InvocationHandler() { @Override public Object invoke(Object o, Method method, Object[] objects) throws Throwable { return ProxyBuilder.callSuper(o, method, objects); } }; FooThrows fooThrows = proxyFor(FooThrows.class) .handler(handler) .build(); try { fooThrows.foo(); fail(); } catch (IllegalStateException expected) { assertEquals("boom!", expected.getMessage()); } } public static class FooThrows { public void foo() { throw new IllegalStateException("boom!"); } } public static class DoubleReturn { public double getValue() { return 2.0; } } @Test public void testUnboxedResult() throws Exception { fakeHandler.fakeResult = 2.0; assertEquals(2.0, proxyFor(DoubleReturn.class).build().getValue(), DELTA_DOUBLE); } public static void staticMethod() {} @Test public void testDoesNotOverrideStaticMethods() throws Exception { // Method should exist on this test class itself. ProxyBuilderTest.class.getDeclaredMethod("staticMethod"); // Method should not exist on the subclass. try { proxyFor(ProxyBuilderTest.class).build().getClass().getDeclaredMethod("staticMethod"); fail(); } catch (NoSuchMethodException expected) {} } @Test(expected = IOException.class) public void testIllegalCacheDirectory() throws Exception { proxyFor(ProxyForIllegalCacheDirectory.class) .dexCache(new File("/poop/")) .build(); } public static class ProxyForIllegalCacheDirectory {} @Test(expected = IllegalArgumentException.class) public void testInvalidConstructorSpecification() throws Exception { proxyFor(Object.class) .constructorArgTypes(String.class, Boolean.class) .constructorArgValues("asdf", true) .build(); } public static abstract class AbstractClass { public abstract Object getValue(); } @Test public void testAbstractClassBehaviour() throws Exception { assertEquals("fake result", proxyFor(AbstractClass.class).build().getValue()); } @Test public void testCallAbstractSuperMethod() throws Exception { AbstractClass a = proxyFor(AbstractClass.class).build(); // Setting the handler to null routes all calls to the real methods. In this case the real // method is abstract and cannot be called ProxyBuilder.setInvocationHandler(a, null); try { a.getValue(); fail(); } catch (AbstractMethodError expected) { } } public static class CtorHasDeclaredException { public CtorHasDeclaredException() throws IOException { throw new IOException(); } } public static class CtorHasRuntimeException { public CtorHasRuntimeException() { throw new RuntimeException("my message"); } } public static class CtorHasError { public CtorHasError() { throw new Error("my message again"); } } @Test public void testParentConstructorThrowsDeclaredException() throws Exception { try { proxyFor(CtorHasDeclaredException.class).build(); fail(); } catch (UndeclaredThrowableException expected) { assertTrue(expected.getCause() instanceof IOException); } try { proxyFor(CtorHasRuntimeException.class).build(); fail(); } catch (RuntimeException expected) { assertEquals("my message", expected.getMessage()); } try { proxyFor(CtorHasError.class).build(); } catch (Error expected) { assertEquals("my message again", expected.getMessage()); return; } fail(); } @Test public void testGetInvocationHandler_NormalOperation() throws Exception { Object proxy = proxyFor(Object.class).build(); assertSame(fakeHandler, ProxyBuilder.getInvocationHandler(proxy)); } @Test(expected = IllegalArgumentException.class) public void testGetInvocationHandler_NotAProxy() { ProxyBuilder.getInvocationHandler(new Object()); } public static class ReturnsObject { public Object getValue() { return new Object(); } } public static class ReturnsString extends ReturnsObject { @Override public String getValue() { return "a string"; } } @Test public void testCovariantReturnTypes_NormalBehaviour() throws Exception { String expected = "some string"; fakeHandler.setFakeResult(expected); assertSame(expected, proxyFor(ReturnsObject.class).build().getValue()); assertSame(expected, proxyFor(ReturnsString.class).build().getValue()); } @Test public void testCovariantReturnTypes_WrongReturnType() throws Exception { try { fakeHandler.setFakeResult(new Object()); proxyFor(ReturnsString.class).build().getValue(); fail(); } catch (ClassCastException expected) {} } @Test public void testCaching() throws Exception { SimpleClass a = proxyFor(SimpleClass.class).build(); SimpleClass b = proxyFor(SimpleClass.class).build(); assertSame(a.getClass(), b.getClass()); } @Test public void testCachingWithMultipleConstructors() throws Exception { HasMultipleConstructors a = ProxyBuilder.forClass(HasMultipleConstructors.class) .constructorArgTypes() .constructorArgValues() .handler(fakeHandler) .dexCache(DexMakerTest.getDataDirectory()).build(); assertEquals("no args", a.calledConstructor); HasMultipleConstructors b = ProxyBuilder.forClass(HasMultipleConstructors.class) .constructorArgTypes(int.class) .constructorArgValues(2) .handler(fakeHandler) .dexCache(DexMakerTest.getDataDirectory()).build(); assertEquals("int 2", b.calledConstructor); assertEquals(a.getClass(), b.getClass()); HasMultipleConstructors c = ProxyBuilder.forClass(HasMultipleConstructors.class) .constructorArgTypes(Integer.class) .constructorArgValues(3) .handler(fakeHandler) .dexCache(DexMakerTest.getDataDirectory()).build(); assertEquals("Integer 3", c.calledConstructor); assertEquals(a.getClass(), c.getClass()); } public static class HasMultipleConstructors { private final String calledConstructor; public HasMultipleConstructors() { calledConstructor = "no args"; } public HasMultipleConstructors(int b) { calledConstructor = "int " + b; } public HasMultipleConstructors(Integer c) { calledConstructor = "Integer " + c; } } @Test public void testClassNotCachedWithDifferentParentClassLoaders() throws Exception { ClassLoader classLoaderA = newPathClassLoader(); SimpleClass a = proxyFor(SimpleClass.class) .parentClassLoader(classLoaderA) .build(); assertEquals(classLoaderA, a.getClass().getClassLoader().getParent()); ClassLoader classLoaderB = newPathClassLoader(); SimpleClass b = proxyFor(SimpleClass.class) .parentClassLoader(classLoaderB) .build(); assertEquals(classLoaderB, b.getClass().getClassLoader().getParent()); assertTrue(a.getClass() != b.getClass()); } @Test public void testAbstractClassWithUndeclaredInterfaceMethod() throws Throwable { DeclaresInterface declaresInterface = proxyFor(DeclaresInterface.class) .build(); assertEquals("fake result", declaresInterface.call()); try { ProxyBuilder.callSuper(declaresInterface, Callable.class.getMethod("call")); fail(); } catch (IncompatibleClassChangeError expected) { } } public static abstract class DeclaresInterface implements Callable {} @Test public void testImplementingInterfaces() throws Throwable { SimpleClass simpleClass = proxyFor(SimpleClass.class) .implementing(Callable.class) .implementing(Comparable.class) .build(); assertEquals("fake result", simpleClass.simpleMethod()); Callable asCallable = (Callable) simpleClass; assertEquals("fake result", asCallable.call()); Comparable asComparable = (Comparable) simpleClass; fakeHandler.fakeResult = 3; assertEquals(3, asComparable.compareTo(null)); } @Test public void testCallSuperWithInterfaceMethod() throws Throwable { SimpleClass simpleClass = proxyFor(SimpleClass.class) .implementing(Callable.class) .build(); try { ProxyBuilder.callSuper(simpleClass, Callable.class.getMethod("call")); fail(); } catch (AbstractMethodError expected) { } catch (NoSuchMethodError expected) { } } @Test public void testImplementInterfaceCallingThroughConcreteClass() throws Throwable { InvocationHandler invocationHandler = new InvocationHandler() { @Override public Object invoke(Object o, Method method, Object[] objects) throws Throwable { assertEquals("a", ProxyBuilder.callSuper(o, method, objects)); return "b"; } }; ImplementsCallable proxy = proxyFor(ImplementsCallable.class) .implementing(Callable.class) .handler(invocationHandler) .build(); assertEquals("b", proxy.call()); assertEquals("a", ProxyBuilder.callSuper( proxy, ImplementsCallable.class.getMethod("call"))); } /** * This test is a bit unintuitive because it exercises the synthetic methods * that support covariant return types. Calling 'Object call()' on the * interface bridges to 'String call()', and so the super method appears to * also be proxied. */ @Test public void testImplementInterfaceCallingThroughInterface() throws Throwable { final AtomicInteger count = new AtomicInteger(); InvocationHandler invocationHandler = new InvocationHandler() { @Override public Object invoke(Object o, Method method, Object[] objects) throws Throwable { count.incrementAndGet(); return ProxyBuilder.callSuper(o, method, objects); } }; Callable proxy = proxyFor(ImplementsCallable.class) .implementing(Callable.class) .handler(invocationHandler) .build(); // the invocation handler is called twice! assertEquals("a", proxy.call()); assertEquals(2, count.get()); // the invocation handler is called, even though this is a callSuper() call! assertEquals("a", ProxyBuilder.callSuper(proxy, Callable.class.getMethod("call"))); assertEquals(3, count.get()); } public static class ImplementsCallable implements Callable { @Override public String call() throws Exception { return "a"; } } /** * This test shows that our generated proxies follow the bytecode convention * where methods can have the same name but unrelated return types. This is * different from javac's convention where return types must be assignable * in one direction or the other. */ @Test public void testInterfacesSameNamesDifferentReturnTypes() throws Throwable { InvocationHandler handler = new InvocationHandler() { @Override public Object invoke(Object o, Method method, Object[] objects) throws Throwable { if (method.getReturnType() == void.class) { return null; } else if (method.getReturnType() == String.class) { return "X"; } else if (method.getReturnType() == int.class) { return 3; } else { throw new AssertionFailedError(); } } }; Object o = proxyFor(Object.class) .implementing(FooReturnsVoid.class, FooReturnsString.class, FooReturnsInt.class) .handler(handler) .build(); FooReturnsVoid a = (FooReturnsVoid) o; a.foo(); FooReturnsString b = (FooReturnsString) o; assertEquals("X", b.foo()); FooReturnsInt c = (FooReturnsInt) o; assertEquals(3, c.foo()); } @Test public void testCallInterfaceSuperMethod() throws Exception { FooReturnsVoid f = (FooReturnsVoid)proxyFor(Object.class).implementing(FooReturnsVoid.class) .build(); // Setting the handler to null routes all calls to the real methods. In this case the real // method is a method of an interface and cannot be called ProxyBuilder.setInvocationHandler(f, null); try { f.foo(); fail(); } catch (AbstractMethodError expected) { } } @Test public void testInterfacesSameNamesSameReturnType() throws Throwable { Object o = proxyFor(Object.class) .implementing(FooReturnsInt.class, FooReturnsInt2.class) .build(); fakeHandler.setFakeResult(3); FooReturnsInt a = (FooReturnsInt) o; assertEquals(3, a.foo()); FooReturnsInt2 b = (FooReturnsInt2) o; assertEquals(3, b.foo()); } public interface FooReturnsVoid { void foo(); } public interface FooReturnsString { String foo(); } public interface FooReturnsInt { int foo(); } public interface FooReturnsInt2 { int foo(); } private ClassLoader newPathClassLoader() throws Exception { return (ClassLoader) Class.forName("dalvik.system.PathClassLoader") .getConstructor(String.class, ClassLoader.class) .newInstance("", getClass().getClassLoader()); } @Test public void testSubclassOfRandom() throws Exception { proxyFor(Random.class) .handler(new InvokeSuperHandler()) .build(); } public static class FinalToString { @Override public final String toString() { return "no proxy"; } } // https://code.google.com/p/dexmaker/issues/detail?id=12 @Test public void testFinalToString() throws Throwable { assertEquals("no proxy", proxyFor(FinalToString.class).build().toString()); } public static class FinalInterfaceImpl implements FooReturnsString { @Override public final String foo() { return "no proxy"; } } public static class ExtenstionOfFinalInterfaceImpl extends FinalInterfaceImpl implements FooReturnsString { } @Test public void testFinalInterfaceImpl() throws Throwable { assertEquals("no proxy", proxyFor(ExtenstionOfFinalInterfaceImpl.class).build().foo()); } // https://code.google.com/p/dexmaker/issues/detail?id=9 public interface DeclaresMethodLate { void thisIsTheMethod(); } public static class MakesMethodFinalEarly { public final void thisIsTheMethod() {} } public static class YouDoNotChooseYourFamily extends MakesMethodFinalEarly implements DeclaresMethodLate {} @Test public void testInterfaceMethodMadeFinalBeforeActualInheritance() throws Exception { proxyFor(YouDoNotChooseYourFamily.class).build(); } public interface ExtendsAnotherInterface extends FooReturnsString { } @Test public void testExtraInterfaceExtendsInterface() throws Exception { ExtendsAnotherInterface proxy = (ExtendsAnotherInterface) proxyFor(SimpleClass.class) .implementing(ExtendsAnotherInterface.class) .build(); fakeHandler.setFakeResult(ExtendsAnotherInterface.class.getName()); assertEquals(ExtendsAnotherInterface.class.getName(), proxy.foo()); } /** Simple helper to add the most common args for this test to the proxy builder. */ private ProxyBuilder proxyFor(Class clazz) throws Exception { return ProxyBuilder.forClass(clazz) .handler(fakeHandler) .dexCache(DexMakerTest.getDataDirectory()); } private static class FakeInvocationHandler implements InvocationHandler { private Object fakeResult = "fake result"; @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { return fakeResult; } public void setFakeResult(Object result) { fakeResult = result; } } public static class TestOrderingClass { public int returnsInt() { return 0; } public int returnsInt(int param1, int param2) { return 1; } public String returnsString() { return "string"; } public boolean returnsBoolean() { return false; } public double returnsDouble() { return 1.0; } public Object returnsObject() { return new Object(); } } @Test @SuppressWarnings("unchecked") public void testMethodsGeneratedInDeterministicOrder() throws Exception { // Grab the static methods array from the original class. Method[] methods1 = getMethodsForProxyClass(TestOrderingClass.class); assertNotNull(methods1); // Clear ProxyBuilder's in-memory cache of classes. This will force // it to rebuild the class and reset the static methods field. Map, Class> map = getGeneratedProxyClasses(); assertNotNull(map); map.clear(); // Grab the static methods array from the rebuilt class. Method[] methods2 = getMethodsForProxyClass(TestOrderingClass.class); assertNotNull(methods2); // Ensure that the two method arrays are equal. assertTrue(Arrays.equals(methods1, methods2)); } @Test public void testOrderingClassWithDexMakerCaching() throws Exception { doTestOrderClassWithDexMakerCaching(); // Force ProxyBuilder to call DexMaker.generateAndLoad() getGeneratedProxyClasses().clear(); doTestOrderClassWithDexMakerCaching(); } private void doTestOrderClassWithDexMakerCaching() throws Exception { TestOrderingClass proxy = ProxyBuilder.forClass(TestOrderingClass.class) .handler(new InvokeSuperHandler()) .dexCache(DexMakerTest.getDataDirectory()) .build(); assertEquals(0, proxy.returnsInt()); assertEquals(1, proxy.returnsInt(1, 1)); assertEquals("string", proxy.returnsString()); assertFalse(proxy.returnsBoolean()); assertEquals(1.0, proxy.returnsDouble(), DELTA_DOUBLE); assertNotNull(proxy.returnsObject()); assertTrue(versionedDxDir.listFiles().length != 0); } // Returns static methods array from a proxy class. private Method[] getMethodsForProxyClass(Class parentClass) throws Exception { Class proxyClass = proxyFor(parentClass).buildProxyClass(); Method[] methods = null; for (Field f : proxyClass.getDeclaredFields()) { if (Method[].class.isAssignableFrom(f.getType())) { f.setAccessible(true); methods = (Method[]) f.get(null); break; } } return methods; } private Map, Class> getGeneratedProxyClasses() throws Exception { Field mapField = ProxyBuilder.class .getDeclaredField("generatedProxyClasses"); mapField.setAccessible(true); return (Map, Class>) mapField.get(null); } public static class ConcreteClassA implements FooReturnsInt { @Override // from FooReturnsInt public int foo() { return 1; } // not from FooReturnsInt public String bar() { return "bar"; } } public static class ConcreteClassB implements FooReturnsInt { @Override // from FooReturnsInt public int foo() { return 0; } // not from FooReturnsInt public String bar() { return "bahhr"; } } @Test public void testTwoClassesWithIdenticalMethodSignatures_DexMakerCaching() throws Exception { ConcreteClassA proxyA = ProxyBuilder.forClass(ConcreteClassA.class) .handler(new InvokeSuperHandler()) .dexCache(DexMakerTest.getDataDirectory()) .build(); assertEquals(1, proxyA.foo()); assertEquals("bar", proxyA.bar()); int numFiles = versionedDxDir.listFiles().length; assertTrue(numFiles > 0); ConcreteClassB proxyB = ProxyBuilder.forClass(ConcreteClassB.class) .handler(new InvokeSuperHandler()) .dexCache(DexMakerTest.getDataDirectory()) .build(); assertEquals(0, proxyB.foo()); assertEquals("bahhr", proxyB.bar()); assertTrue(numFiles < versionedDxDir.listFiles().length); } public static abstract class PartiallyFinalClass { public String returnA() { return "A"; } public String returnB() { return "B"; } public String returnC() { return "C"; } public final String returnD() { return "D"; } public abstract String returnE(); } @Test public void testProxyingSomeMethods() throws Throwable { ArrayList methodsToOverride = new ArrayList<>(); for (Method method : PartiallyFinalClass.class.getDeclaredMethods()) { if (!Modifier.isFinal(method.getModifiers()) && !method.getName().equals("returnC")) { methodsToOverride.add(method); } } InvocationHandler handler = new InvokeSuperHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (method.getName().equals("returnA")) { return "fake A"; } else if (method.getName().equals("returnC")) { // This will never trigger as "returnC" is not overridden return "fake C"; } else if (method.getName().equals("returnE")) { return "fake E"; } else { return super.invoke(proxy, method, args); } } }; PartiallyFinalClass proxy = ProxyBuilder.forClass(PartiallyFinalClass.class) .handler(handler).onlyMethods(methodsToOverride.toArray(new Method[]{})).build(); assertEquals("fake A", proxy.returnA()); assertEquals("B", proxy.returnB()); assertEquals("C", proxy.returnC()); assertEquals("D", proxy.returnD()); assertEquals("fake E", proxy.returnE()); } @Test public void testImplementingDifferentInterfacesWithSharedClassLoader() throws IOException { assumeTrue(Build.VERSION.SDK_INT >= 24); Class c1 = ProxyBuilder.forClass(SimpleClass.class) .implementing(Runnable.class).withSharedClassLoader().buildProxyClass(); Class c2 = ProxyBuilder.forClass(SimpleClass.class) .implementing(Callable.class).withSharedClassLoader().buildProxyClass(); assertNotSame(c1, c2); } @Test public void testInterfaceOrder() throws Exception { Class c1 = proxyFor(SimpleClass.class) .implementing(Runnable.class, Callable.class).buildProxyClass(); assertEquals(new Class[]{Runnable.class, Callable.class}, c1.getInterfaces()); Class c2 = proxyFor(SimpleClass.class) .implementing(Callable.class, Runnable.class).buildProxyClass(); assertEquals(new Class[]{Callable.class, Runnable.class}, c2.getInterfaces()); } }