diff options
author | Philip P. Moltmann <moltmann@google.com> | 2017-11-20 09:38:54 -0800 |
---|---|---|
committer | Philip P. Moltmann <moltmann@google.com> | 2017-12-12 12:50:10 -0800 |
commit | 171f097997993b84053f643dc275ce66364315ca (patch) | |
tree | ff5169be265d1ee674c4bd7748d52ca15b5ce20a | |
parent | 61a444d2c7f6c6a81ca2bf54958b02b7cdc81c04 (diff) | |
download | dexmaker-171f097997993b84053f643dc275ce66364315ca.tar.gz |
Update dexmaker to latest (master) state
This adds dexmaker-mockito-inline support and dexmaker-mockito-*-tests.
Inline mocking will allow mocking of final methods/classes.
Bug: 63538681
Test: cts-tradefed run cts-dev -m CtsMockingTestCases
Change-Id: Id8b88639abe0a84642273eae43322df09068a782
45 files changed, 4467 insertions, 111 deletions
@@ -38,6 +38,96 @@ java_library_static { ], } +// Build dispatcher for Dexmaker's inline MockMaker +java_library_static { + name: "dexmaker-inline-mockmaker-dispatcher", + sdk_version: "25", + srcs: ["dexmaker-mockito-inline-dispatcher/src/main/java/**/*.java"], +} + +// Build agent for Dexmaker's inline MockMaker +cc_library_shared { + name: "libdexmakerjvmtiagent", + srcs: ["dexmaker-mockito-inline/src/main/jni/**/*.cc"], + + host_supported: false, + device_supported: true, + + rtti: true, + + cflags: [ + "-std=c++11", + "-Wall", + "-Werror", + "-Wno-unused-parameter", + "-Wno-shift-count-overflow", + "-Wno-error=non-virtual-dtor", + "-Wno-sign-compare", + "-Wno-switch", + "-Wno-missing-braces", + ], + + static_libs: [ + "slicer", + ], + + shared_libs: [ + "libz", + ], + + header_libs: [ + "libopenjdkjvmti_headers", + ], +} + +// Build agent for Dexmaker's inline tests +cc_library_shared { + name: "libmultiplejvmtiagentsinterferenceagent", + srcs: ["dexmaker-mockito-inline-tests/src/main/jni/**/*.cc"], + + host_supported: false, + device_supported: true, + + rtti: true, + + cflags: [ + "-std=c++11", + "-Wall", + "-Werror", + "-Wno-unused-parameter", + "-Wno-shift-count-overflow", + "-Wno-error=non-virtual-dtor", + "-Wno-sign-compare", + "-Wno-switch", + "-Wno-missing-braces", + ], + + static_libs: [ + "slicer", + ], + + shared_libs: [ + "libz", + ], + + header_libs: [ + "libopenjdkjvmti_headers", + ], +} + +// Build Dexmaker's inline MockMaker, a plugin to Mockito +java_library_static { + name: "dexmaker-inline-mockmaker", + sdk_version: "25", + srcs: ["dexmaker-mockito-inline/src/main/java/**/*.java"], + java_resource_dirs: ["dexmaker-mockito-inline/src/main/resources"], + libs: [ + "dexmaker", + "mockito-api", + ], + required: ["libdexmakerjvmtiagent"], +} + java_import { name: "dexmaker-dx-target", jars: ["lib/dalvik-dx-1.jar"], diff --git a/README.version b/README.version index 0fb9575..6708f4b 100644 --- a/README.version +++ b/README.version @@ -1,5 +1,5 @@ -URL: https://github.com/crittercism/dexmaker/ -Version: 2.2.0 +URL: https://github.com/linkedin/dexmaker/ +Version: master (d4959c215e3e2a92b478ddc72a2692cb40f3efd3) License: Apache 2.0 Description: Dexmaker is a Java-language API for doing compile time or runtime code generation targeting the Dalvik VM. Unlike cglib or ASM, this library creates Dalvik .dex files instead of Java .class files. @@ -9,7 +9,4 @@ It has a small, close-to-the-metal API. This API mirrors the Dalvik bytecode spe It includes a stock code generator for class proxies. If you just want to do AOP or class mocking, you don't need to mess around with bytecodes. Local Modifications: - Support mocking of package private classes using dexmaker.share_classloader - Scan for methods in extra interface hierarchy - Update stack trace cleaner to use new dex package name - Update stack trace cleaner to filter out java.lang.reflect.Proxy calls + None diff --git a/dexmaker-mockito-inline-dispatcher/build.gradle b/dexmaker-mockito-inline-dispatcher/build.gradle new file mode 100644 index 0000000..c9667c0 --- /dev/null +++ b/dexmaker-mockito-inline-dispatcher/build.gradle @@ -0,0 +1,17 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 25 + buildToolsVersion "25.0.0" + + lintOptions { + abortOnError false + } + + defaultConfig { + applicationId "com.android.dexmaker.mockito.inline.dispatcher" + minSdkVersion 25 + targetSdkVersion 25 + versionName VERSION_NAME + } +}
\ No newline at end of file diff --git a/dexmaker-mockito-inline-dispatcher/src/main/AndroidManifest.xml b/dexmaker-mockito-inline-dispatcher/src/main/AndroidManifest.xml new file mode 100644 index 0000000..75024f1 --- /dev/null +++ b/dexmaker-mockito-inline-dispatcher/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ +<manifest package="com.android.dexmaker.mockito.inline.dispatcher"> + <application /> +</manifest> diff --git a/dexmaker-mockito-inline-dispatcher/src/main/java/com/android/dx/mockito/inline/MockMethodDispatcher.java b/dexmaker-mockito-inline-dispatcher/src/main/java/com/android/dx/mockito/inline/MockMethodDispatcher.java new file mode 100644 index 0000000..c693992 --- /dev/null +++ b/dexmaker-mockito-inline-dispatcher/src/main/java/com/android/dx/mockito/inline/MockMethodDispatcher.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2017 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.mockito.inline; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.concurrent.Callable; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * Called by method entry hooks. Dispatches these hooks to the {@code MockMethodAdvice}. + */ +@SuppressWarnings("unused") +public class MockMethodDispatcher { + // An instance of {@code MockMethodAdvice} + private Object mAdvice; + + // All dispatchers for various identifiers + private static final ConcurrentMap<String, MockMethodDispatcher> INSTANCE = + new ConcurrentHashMap<>(); + + /** + * Get the dispatcher for a identifier. + * + * @param identifier identifier of the dispatcher + * @param instance instance that might be mocked + * + * @return dispatcher for the identifier + */ + public static MockMethodDispatcher get(String identifier, Object instance) { + if (instance == INSTANCE) { + // Avoid endless loop if ConcurrentHashMap was redefined to check for being a mock. + return null; + } else { + return INSTANCE.get(identifier); + } + } + + /** + * Create a new dispatcher. + * + * @param advice Advice the dispatcher should call + */ + private MockMethodDispatcher(Object advice) { + mAdvice = advice; + } + + /** + * Set up a new advice to receive calls for an identifier + * + * @param identifier a unique identifier + * @param advice advice the dispatcher should call + */ + public static void set(String identifier, Object advice) { + INSTANCE.putIfAbsent(identifier, new MockMethodDispatcher(advice)); + } + + /** + * Calls {@code MockMethodAdvice#handle} + */ + public Callable<?> handle(Object instance, Method origin, Object[] arguments) throws Throwable { + try { + return (Callable<?>) mAdvice.getClass().getMethod("handle", Object.class, Method.class, + Object[].class).invoke(mAdvice, instance, origin, arguments); + } catch (InvocationTargetException e) { + throw e.getCause(); + } + } + + /** + * Calls {@code MockMethodAdvice#isMock} + */ + public boolean isMock(Object instance) { + try { + return (Boolean) mAdvice.getClass().getMethod("isMock", Object.class).invoke(mAdvice, + instance); + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new IllegalStateException(e); + } + } + + /** + * Calls {@code MockMethodAdvice#isMocked} + */ + public boolean isMocked(Object instance) { + try { + return (Boolean) mAdvice.getClass().getMethod("isMocked", Object.class).invoke(mAdvice, + instance); + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new IllegalStateException(e); + } + } + + /** + * Calls {@code MockMethodAdvice#isOverridden} + */ + public boolean isOverridden(Object instance, Method origin) { + try { + return (Boolean) mAdvice.getClass().getMethod("isOverridden", Object.class, + Method.class).invoke(mAdvice, instance, origin); + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new IllegalStateException(e); + } + } + + /** + * Calls {@code MockMethodAdvice#getOrigin} + */ + public Method getOrigin(Object mock, String instrumentedMethodWithTypeAndSignature) + throws Throwable { + return (Method) mAdvice.getClass().getMethod("getOrigin", Object.class, + String.class).invoke(mAdvice, mock, instrumentedMethodWithTypeAndSignature); + } +} diff --git a/dexmaker-mockito-inline-tests/AndroidManifest.xml b/dexmaker-mockito-inline-tests/AndroidManifest.xml new file mode 100644 index 0000000..44afd30 --- /dev/null +++ b/dexmaker-mockito-inline-tests/AndroidManifest.xml @@ -0,0 +1,3 @@ +<manifest package="com.android.dexmaker.mockito.inline.tests"> + <application /> +</manifest> diff --git a/dexmaker-mockito-inline-tests/src/androidTest/java/com/android/dx/mockito/inline/tests/CleanStackTrace.java b/dexmaker-mockito-inline-tests/src/androidTest/java/com/android/dx/mockito/inline/tests/CleanStackTrace.java new file mode 100644 index 0000000..e90145f --- /dev/null +++ b/dexmaker-mockito-inline-tests/src/androidTest/java/com/android/dx/mockito/inline/tests/CleanStackTrace.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2017 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.mockito.inline.tests; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +public class CleanStackTrace { + public abstract static class TestAbstractClass { + public abstract String returnA(); + } + + public static class TestClass { + public final String returnA() { + return "A"; + } + } + + public interface TestInterface { + String returnA(); + } + + @Test + public void cleanStackTraceProxy() { + TestAbstractClass t = mock(TestAbstractClass.class); + + try { + verify(t).returnA(); + } catch (Throwable verifyLocation) { + try { + throw new Exception(); + } catch (Exception here) { + assertEquals(here.getStackTrace()[0].getMethodName(), verifyLocation + .getStackTrace()[0].getMethodName()); + } + } + } + + @Test + public void cleanStackTraceInline() { + TestClass t = mock(TestClass.class); + + try { + verify(t).returnA(); + } catch (Throwable verifyLocation) { + try { + throw new Exception(); + } catch (Exception here) { + assertEquals(here.getStackTrace()[0].getMethodName(), verifyLocation + .getStackTrace()[1].getMethodName()); + } + } + } + + @Test + public void cleanStackTraceInterface() { + TestInterface t = mock(TestInterface.class); + + try { + verify(t).returnA(); + } catch (Throwable verifyLocation) { + try { + throw new Exception(); + } catch (Exception here) { + assertEquals(here.getStackTrace()[0].getMethodName(), verifyLocation + .getStackTrace()[0].getMethodName()); + } + } + } +} diff --git a/dexmaker-mockito-inline-tests/src/androidTest/java/com/android/dx/mockito/inline/tests/MockFinal.java b/dexmaker-mockito-inline-tests/src/androidTest/java/com/android/dx/mockito/inline/tests/MockFinal.java new file mode 100644 index 0000000..fa02471 --- /dev/null +++ b/dexmaker-mockito-inline-tests/src/androidTest/java/com/android/dx/mockito/inline/tests/MockFinal.java @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2017 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.mockito.inline.tests; + +import android.content.Intent; +import android.os.IBinder; +import android.print.PrintAttributes; +import android.printservice.PrintService; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +@RunWith(AndroidJUnit4.class) +public class MockFinal { + @Test + public void mockFinalJavaMethod() throws Exception { + ClassLoader fakeParent = mock(ClassLoader.class); + ClassLoader mockClassLoader = mock(ClassLoader.class); + + assertNull(mockClassLoader.getParent()); + + // ClassLoader#getParent is final + when(mockClassLoader.getParent()).thenReturn(fakeParent); + + assertSame(fakeParent, mockClassLoader.getParent()); + } + + @Test + public void mockFinalAndroidFrameworkClass() throws Exception { + // PrintAttributes is final + PrintAttributes mockAttributes = mock(PrintAttributes.class); + + assertEquals(0, mockAttributes.getColorMode()); + + when(mockAttributes.getColorMode()).thenReturn(42); + + assertEquals(42, mockAttributes.getColorMode()); + } + + @Test + public void mockFinalMethodOfAndroidFrameworkClass() throws Exception { + IBinder fakeBinder = mock(IBinder.class); + PrintService mockService = mock(PrintService.class); + + assertNull(mockService.onBind(new Intent())); + + // PrintService#onBind is final + when(mockService.onBind(any(Intent.class))).thenReturn(fakeBinder); + + assertSame(fakeBinder, mockService.onBind(new Intent())); + } + + private final class FinalNonDefaultConstructorClass { + public FinalNonDefaultConstructorClass(int i) { + } + + String returnA() { + return "A"; + } + } + + @Test + public void mockNonDefaultConstructorClass() throws Exception { + FinalNonDefaultConstructorClass mock = mock(FinalNonDefaultConstructorClass.class); + + assertNull(mock.returnA()); + when(mock.returnA()).thenReturn("fakeA"); + + assertEquals("fakeA", mock.returnA()); + } + + private interface NonDefaultConstructorInterface { + String returnA(); + } + + @Test + public void mockNonDefaultConstructorInterface() throws Exception { + NonDefaultConstructorInterface mock = mock(NonDefaultConstructorInterface.class); + + assertNull(mock.returnA()); + when(mock.returnA()).thenReturn("fakeA"); + + assertEquals("fakeA", mock.returnA()); + } + + private static class SuperClass { + final String returnA() { + return "superA"; + } + + String returnB() { + return "superB"; + } + + String returnC() { + return "superC"; + } + } + + private static final class SubClass extends SuperClass { + String returnC() { + return "subC"; + } + } + + @Test + public void mockSubClass() throws Exception { + SubClass mocked = mock(SubClass.class); + SuperClass mockedSuper = mock(SuperClass.class); + SubClass nonMocked = new SubClass(); + SuperClass nonMockedSuper = new SuperClass(); + + // Mock returns dummy value by default + assertNull(mocked.returnA()); + assertNull(mocked.returnB()); + assertNull(mocked.returnC()); + assertNull(mockedSuper.returnA()); + assertNull(mockedSuper.returnB()); + assertNull(mockedSuper.returnC()); + + // Set fake values for mockedSuper + when(mockedSuper.returnA()).thenReturn("fakeA"); + when(mockedSuper.returnB()).thenReturn("fakeB"); + when(mockedSuper.returnC()).thenReturn("fakeC"); + + // mocked is unaffected + assertNull(mocked.returnA()); + assertNull(mocked.returnB()); + assertNull(mocked.returnC()); + + // Verify fake values of mockedSuper + assertEquals("fakeA", mockedSuper.returnA()); + assertEquals("fakeB", mockedSuper.returnB()); + assertEquals("fakeC", mockedSuper.returnC()); + + // Set fake values for mocked + when(mocked.returnA()).thenReturn("fake2A"); + when(mocked.returnB()).thenReturn("fake2B"); + when(mocked.returnC()).thenReturn("fake2C"); + + // Verify fake values of mocked + assertEquals("fake2A", mocked.returnA()); + assertEquals("fake2B", mocked.returnB()); + assertEquals("fake2C", mocked.returnC()); + + // non mocked instances are unaffected + assertEquals("superA", nonMocked.returnA()); + assertEquals("superB", nonMocked.returnB()); + assertEquals("subC", nonMocked.returnC()); + assertEquals("superA", nonMockedSuper.returnA()); + assertEquals("superB", nonMockedSuper.returnB()); + assertEquals("superC", nonMockedSuper.returnC()); + } + + @Test + public void spySubClass() throws Exception { + SubClass spied = spy(SubClass.class); + SuperClass spiedSuper = spy(SuperClass.class); + SubClass nonSpied = new SubClass(); + SuperClass nonSpiedSuper = new SuperClass(); + + // Spies call real method by default + assertEquals("superA", spied.returnA()); + assertEquals("superB", spied.returnB()); + assertEquals("subC", spied.returnC()); + assertEquals("superA", spiedSuper.returnA()); + assertEquals("superB", spiedSuper.returnB()); + assertEquals("superC", spiedSuper.returnC()); + + // Set fake values for spiedSuper + when(spiedSuper.returnA()).thenReturn("fakeA"); + when(spiedSuper.returnB()).thenReturn("fakeB"); + when(spiedSuper.returnC()).thenReturn("fakeC"); + + // spied is unaffected + assertEquals("superA", spied.returnA()); + assertEquals("superB", spied.returnB()); + assertEquals("subC", spied.returnC()); + + // Verify fake values of spiedSuper + assertEquals("fakeA", spiedSuper.returnA()); + assertEquals("fakeB", spiedSuper.returnB()); + assertEquals("fakeC", spiedSuper.returnC()); + + // Set fake values for spied + when(spied.returnA()).thenReturn("fake2A"); + when(spied.returnB()).thenReturn("fake2B"); + when(spied.returnC()).thenReturn("fake2C"); + + // Verify fake values of spied + assertEquals("fake2A", spied.returnA()); + assertEquals("fake2B", spied.returnB()); + assertEquals("fake2C", spied.returnC()); + + // non spied instances are unaffected + assertEquals("superA", nonSpied.returnA()); + assertEquals("superB", nonSpied.returnB()); + assertEquals("subC", nonSpied.returnC()); + assertEquals("superA", nonSpiedSuper.returnA()); + assertEquals("superB", nonSpiedSuper.returnB()); + assertEquals("superC", nonSpiedSuper.returnC()); + } +} diff --git a/dexmaker-mockito-inline-tests/src/androidTest/java/com/android/dx/mockito/inline/tests/MockNonPublic.java b/dexmaker-mockito-inline-tests/src/androidTest/java/com/android/dx/mockito/inline/tests/MockNonPublic.java new file mode 100644 index 0000000..aa828e5 --- /dev/null +++ b/dexmaker-mockito-inline-tests/src/androidTest/java/com/android/dx/mockito/inline/tests/MockNonPublic.java @@ -0,0 +1,391 @@ +/* + * Copyright (C) 2017 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.mockito.inline.tests; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +public class MockNonPublic { + private interface SingleMethodInterface { + String returnA(); + } + + private static <T extends Class> void mockSingleMethod(T clazz) { + SingleMethodInterface c = (SingleMethodInterface) mock(clazz); + assertNull(c.returnA()); + + when(c.returnA()).thenReturn("fakeA"); + assertEquals("fakeA", c.returnA()); + } + + private static <T extends Class> void spySingleMethod(T clazz) { + SingleMethodInterface c = (SingleMethodInterface) spy(clazz); + assertEquals("A", c.returnA()); + + when(c.returnA()).thenReturn("fakeA"); + assertEquals("fakeA", c.returnA()); + } + + private static <T extends SingleMethodInterface> void spyWrappedSingleMethod(T original) { + T c = spy(original); + assertEquals("A", c.returnA()); + + when(c.returnA()).thenReturn("fakeA"); + assertEquals("fakeA", c.returnA()); + + // original is unaffected + assertEquals("A", original.returnA()); + } + + private interface DualMethodInterface { + String returnA(); + String returnB(); + } + + private static <T extends Class> void mockDualMethod(T clazz) { + DualMethodInterface c = (DualMethodInterface) mock(clazz); + assertNull(c.returnA()); + assertNull(c.returnB()); + + when(c.returnA()).thenReturn("fakeA"); + assertEquals("fakeA", c.returnA()); + assertNull(c.returnB()); + + when(c.returnB()).thenReturn("fakeB"); + assertEquals("fakeA", c.returnA()); + assertEquals("fakeB", c.returnB()); + } + + private static <T extends Class> void spyDualMethod(T clazz) { + DualMethodInterface c = (DualMethodInterface) spy(clazz); + assertEquals("A", c.returnA()); + assertEquals("B", c.returnB()); + + when(c.returnA()).thenReturn("fakeA"); + assertEquals("fakeA", c.returnA()); + assertEquals("B", c.returnB()); + + when(c.returnB()).thenReturn("fakeB"); + assertEquals("fakeA", c.returnA()); + assertEquals("fakeB", c.returnB()); + } + + private static <T extends DualMethodInterface> void spyWrappedDualMethod(T original) { + T c = spy(original); + assertEquals("A", c.returnA()); + assertEquals("B", c.returnB()); + + when(c.returnA()).thenReturn("fakeA"); + assertEquals("fakeA", c.returnA()); + assertEquals("B", c.returnB()); + + when(c.returnB()).thenReturn("fakeB"); + assertEquals("fakeA", c.returnA()); + assertEquals("fakeB", c.returnB()); + + // original is unaffected + assertEquals("A", original.returnA()); + assertEquals("B", original.returnB()); + } + + private static class PrivateClass implements SingleMethodInterface { + public String returnA() { + return "A"; + } + } + + @Test + public void mockPrivateClass() { + mockSingleMethod(PrivateClass.class); + } + + @Test + public void spyPrivateClass() { + spySingleMethod(PrivateClass.class); + } + + @Test + public void spyWrappedPrivateClass() { + spyWrappedSingleMethod(new PrivateClass()); + } + + private interface PrivateInterface extends SingleMethodInterface { + String returnA(); + } + + @Test + public void mockPrivateInterface() { + mockSingleMethod(PrivateInterface.class); + } + + private static class SubOfPrivateInterface implements PrivateInterface { + public String returnA() { + return "A"; + } + } + + @Test + public void mockSubOfPrivateInterface() { + mockSingleMethod(SubOfPrivateInterface.class); + } + + @Test + public void spySubOfPrivateInterface() { + spySingleMethod(SubOfPrivateInterface.class); + } + + @Test + public void spyWrappedSubOfPrivateInterface() { + spyWrappedSingleMethod(new SubOfPrivateInterface()); + } + + private static abstract class PrivateAbstractClass implements DualMethodInterface { + public String returnA() { + return "A"; + } + + public abstract String returnB(); + } + + @Test + public void mockPrivateAbstractClass() { + mockDualMethod(PrivateAbstractClass.class); + } + + private static class SubOfPrivateAbstractClass extends PrivateAbstractClass { + public String returnB() { + return "B"; + } + } + + @Test + public void mockSubOfPrivateAbstractClass() { + mockDualMethod(SubOfPrivateAbstractClass.class); + } + + @Test + public void spySubOfPrivateAbstractClass() { + spyDualMethod(SubOfPrivateAbstractClass.class); + } + + @Test + public void spyWrappedSubOfPrivateAbstractClass() { + spyWrappedDualMethod(new SubOfPrivateAbstractClass()); + } + + static class PackagePrivateClass implements SingleMethodInterface { + public String returnA() { + return "A"; + } + } + + @Test + public void mockPackagePrivateClass() { + mockSingleMethod(PackagePrivateClass.class); + } + + static abstract class PackagePrivateAbstractClass implements DualMethodInterface { + public String returnA() { + return "A"; + } + + public abstract String returnB(); + } + + @Test + public void mockPackagePrivateAbstractClass() { + mockDualMethod(PackagePrivateAbstractClass.class); + } + + static class SubOfPackagePrivateAbstractClass extends PackagePrivateAbstractClass { + public String returnB() { + return "B"; + } + } + + @Test + public void mockSubOfPackagePrivateAbstractClass() { + mockDualMethod(SubOfPackagePrivateAbstractClass.class); + } + + @Test + public void spySubOfPackagePrivateAbstractClass() { + spyDualMethod(SubOfPackagePrivateAbstractClass.class); + } + + @Test + public void spyWrappedSubOfPackagePrivateAbstractClass() { + spyWrappedDualMethod(new SubOfPackagePrivateAbstractClass()); + } + + interface PackagePrivateInterface extends SingleMethodInterface { + String returnA(); + } + + @Test + public void mockPackagePrivateInterface() { + mockSingleMethod(PackagePrivateInterface.class); + } + + static class SubOfPackagePrivateInterface implements PackagePrivateInterface { + public String returnA() { + return "A"; + } + } + + @Test + public void mockSubOfPackagePrivateInterface() { + mockSingleMethod(SubOfPackagePrivateInterface.class); + } + + @Test + public void spySubOfPackagePrivateInterface() { + spySingleMethod(SubOfPackagePrivateInterface.class); + } + + @Test + public void spyWrappedSubOfPackagePrivateInterface() { + spyWrappedSingleMethod(new SubOfPackagePrivateInterface()); + } + + // Cannot implement SingleMethodInterface as returnA would have to be public + public static class ClassWithPackagePrivateMethod { + String returnA() { + return "A"; + } + } + + @Test + public void mockClassWithPackagePrivateMethod() { + ClassWithPackagePrivateMethod c = mock(ClassWithPackagePrivateMethod.class); + assertNull(c.returnA()); + + when(c.returnA()).thenReturn("fakeA"); + assertEquals("fakeA", c.returnA()); + } + + @Test + public void spyClassWithPackagePrivateMethod() { + ClassWithPackagePrivateMethod c = spy(ClassWithPackagePrivateMethod.class); + assertEquals("A", c.returnA()); + + when(c.returnA()).thenReturn("fakeA"); + assertEquals("fakeA", c.returnA()); + } + + @Test + public void spyWrappedClassWithPackagePrivateMethod() { + ClassWithPackagePrivateMethod original = new ClassWithPackagePrivateMethod(); + ClassWithPackagePrivateMethod c = spy(original); + assertEquals("A", c.returnA()); + + when(c.returnA()).thenReturn("fakeA"); + assertEquals("fakeA", c.returnA()); + + // original is unaffected + assertEquals("A", original.returnA()); + } + + // Cannot implement DualMethodInterface as returnA/returnB would have to be public + public static abstract class AbstractClassWithPackagePrivateMethod { + String returnA() { + return "A"; + } + + abstract String returnB(); + } + + @Test + public void mockAbstractClassWithPackagePrivateMethod() { + AbstractClassWithPackagePrivateMethod c = mock(AbstractClassWithPackagePrivateMethod.class); + assertNull(c.returnA()); + assertNull(c.returnB()); + + when(c.returnA()).thenReturn("fakeA"); + assertEquals("fakeA", c.returnA()); + assertNull(c.returnB()); + + when(c.returnB()).thenReturn("fakeB"); + assertEquals("fakeA", c.returnA()); + assertEquals("fakeB", c.returnB()); + } + + public static class SubOfAbstractClassWithPackagePrivateMethod extends + AbstractClassWithPackagePrivateMethod { + String returnB() { + return "B"; + } + } + + @Test + public void mockSubOfAbstractClassWithPackagePrivateMethod() { + SubOfAbstractClassWithPackagePrivateMethod c = mock + (SubOfAbstractClassWithPackagePrivateMethod.class); + assertNull(c.returnA()); + assertNull(c.returnB()); + + when(c.returnA()).thenReturn("fakeA"); + assertEquals("fakeA", c.returnA()); + assertNull(c.returnB()); + + when(c.returnB()).thenReturn("fakeB"); + assertEquals("fakeA", c.returnA()); + assertEquals("fakeB", c.returnB()); + } + + @Test + public void spySubOfAbstractClassWithPackagePrivateMethod() { + SubOfAbstractClassWithPackagePrivateMethod c = spy + (SubOfAbstractClassWithPackagePrivateMethod.class); + assertEquals("A", c.returnA()); + assertEquals("B", c.returnB()); + + when(c.returnA()).thenReturn("fakeA"); + assertEquals("fakeA", c.returnA()); + assertEquals("B", c.returnB()); + + when(c.returnB()).thenReturn("fakeB"); + assertEquals("fakeA", c.returnA()); + assertEquals("fakeB", c.returnB()); + } + + @Test + public void spyWrappedSubOfAbstractClassWithPackagePrivateMethod() { + SubOfAbstractClassWithPackagePrivateMethod original = new + SubOfAbstractClassWithPackagePrivateMethod(); + SubOfAbstractClassWithPackagePrivateMethod c = spy(original); + assertEquals("A", c.returnA()); + assertEquals("B", c.returnB()); + + when(c.returnA()).thenReturn("fakeA"); + assertEquals("fakeA", c.returnA()); + assertEquals("B", c.returnB()); + + when(c.returnB()).thenReturn("fakeB"); + assertEquals("fakeA", c.returnA()); + assertEquals("fakeB", c.returnB()); + + // original is unaffected + assertEquals("A", original.returnA()); + assertEquals("B", original.returnB()); + } +} diff --git a/dexmaker-mockito-inline-tests/src/androidTest/java/com/android/dx/mockito/inline/tests/MultipleJvmtiAgentsInterference.java b/dexmaker-mockito-inline-tests/src/androidTest/java/com/android/dx/mockito/inline/tests/MultipleJvmtiAgentsInterference.java new file mode 100644 index 0000000..d66b128 --- /dev/null +++ b/dexmaker-mockito-inline-tests/src/androidTest/java/com/android/dx/mockito/inline/tests/MultipleJvmtiAgentsInterference.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2017 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.mockito.inline.tests; + +import android.os.Build; + +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; + +import dalvik.system.BaseDexClassLoader; + +import static org.junit.Assert.assertNull; +import static org.junit.Assume.assumeTrue; +import static org.mockito.Mockito.mock; + +public class MultipleJvmtiAgentsInterference { + private static final String AGENT_LIB_NAME = "multiplejvmtiagentsinterferenceagent"; + + public class TestClass { + public String returnA() { + return "A"; + } + } + + @BeforeClass + public static void installTestAgent() throws Exception { + // TODO (moltmann@google.com): Replace with proper check for >= P + assumeTrue(Build.VERSION.CODENAME.equals("P")); + + // Currently Debug.attachJvmtiAgent requires a file in the right directory + File copiedAgent = File.createTempFile("testagent", ".so"); + copiedAgent.deleteOnExit(); + + try (InputStream is = new FileInputStream(((BaseDexClassLoader) + MultipleJvmtiAgentsInterference.class.getClassLoader()).findLibrary + (AGENT_LIB_NAME))) { + try (OutputStream os = new FileOutputStream(copiedAgent)) { + byte[] buffer = new byte[64 * 1024]; + + while (true) { + int numRead = is.read(buffer); + if (numRead == -1) { + break; + } + os.write(buffer, 0, numRead); + } + } + } + + // TODO (moltmann@google.com): Replace with regular method call once the API becomes public + Class.forName("android.os.Debug").getMethod("attachJvmtiAgent", String.class, String + .class).invoke(null, copiedAgent.getAbsolutePath(), null); + } + + @Test + public void otherAgentTransformsWhileMocking() { + TestClass t = mock(TestClass.class); + + assertNull(t.returnA()); + + // Unrelated class re-transform does not affect mocking + nativeRetransformClasses(new Class<?>[]{MultipleJvmtiAgentsInterference.class}); + assertNull(t.returnA()); + + // Re-transform of classes that are mocked does not affect mocking + nativeRetransformClasses(new Class<?>[]{TestClass.class}); + assertNull(t.returnA()); + } + + private native int nativeRetransformClasses(Class<?>[] classes); +} diff --git a/dexmaker-mockito-inline-tests/src/main/jni/multiplejvmtiagentsinterferenceagent/agent.cc b/dexmaker-mockito-inline-tests/src/main/jni/multiplejvmtiagentsinterferenceagent/agent.cc new file mode 100644 index 0000000..a293fe7 --- /dev/null +++ b/dexmaker-mockito-inline-tests/src/main/jni/multiplejvmtiagentsinterferenceagent/agent.cc @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2017 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. + */ + +#include <jni.h> + +#include <cstring> +#include <cstdlib> +#include <sstream> + +#include "jvmti.h" + +#include <dex_ir.h> +#include <writer.h> +#include <reader.h> + +using namespace dex; + +namespace com_android_dx_mockito_inline_tests { + static jvmtiEnv *localJvmtiEnv; + + // Converts a class name to a type descriptor + // (ex. "java.lang.String" to "Ljava/lang/String;") + static std::string + ClassNameToDescriptor(const char* class_name) { + std::stringstream ss; + ss << "L"; + for (auto p = class_name; *p != '\0'; ++p) { + ss << (*p == '.' ? '/' : *p); + } + ss << ";"; + return ss.str(); + } + + static void + Transform(jvmtiEnv *jvmti_env, + JNIEnv *env, + jclass classBeingRedefined, + jobject loader, + const char *name, + jobject protectionDomain, + jint classDataLen, + const unsigned char *classData, + jint *newClassDataLen, + unsigned char **newClassData) { + // Isolate byte code of class class. This is needed as Android usually gives us more + // than the class we need. + // Then just return the isolated byte code without modification. + Reader reader(classData, (size_t) classDataLen); + + u4 index = reader.FindClassIndex(ClassNameToDescriptor(name).c_str()); + reader.CreateClassIr(index); + std::shared_ptr<ir::DexFile> ir = reader.GetIr(); + + class Allocator : public Writer::Allocator { + jvmtiEnv *jvmti_env; + + public: + Allocator(jvmtiEnv *jvmti_env) : Writer::Allocator(), jvmti_env(jvmti_env) { + } + + virtual void *Allocate(size_t size) { + unsigned char *mem; + jvmti_env->Allocate(size, &mem); + return mem; + } + + virtual void Free(void *ptr) { ::free(ptr); } + }; + + Allocator allocator(jvmti_env); + Writer writer(ir); + size_t newClassLen; + *newClassData = writer.CreateImage(&allocator, &newClassLen); + *newClassDataLen = (jint) newClassLen; + } + + // Initializes the agent + extern "C" jint Agent_OnAttach(JavaVM *vm, + char *options, + void *reserved) { + jint jvmError = vm->GetEnv(reinterpret_cast<void **>(&localJvmtiEnv), JVMTI_VERSION_1_2); + if (jvmError != JNI_OK) { + return jvmError; + } + + jvmtiCapabilities caps; + memset(&caps, 0, sizeof(caps)); + caps.can_retransform_classes = 1; + + jvmtiError error = localJvmtiEnv->AddCapabilities(&caps); + if (error != JVMTI_ERROR_NONE) { + return error; + } + + jvmtiEventCallbacks cb; + memset(&cb, 0, sizeof(cb)); + cb.ClassFileLoadHook = Transform; + + error = localJvmtiEnv->SetEventCallbacks(&cb, sizeof(cb)); + if (error != JVMTI_ERROR_NONE) { + return error; + } + + error = localJvmtiEnv->SetEventNotificationMode(JVMTI_ENABLE, + JVMTI_EVENT_CLASS_FILE_LOAD_HOOK, + NULL); + if (error != JVMTI_ERROR_NONE) { + return error; + } + + return JVMTI_ERROR_NONE; + } + + + // Triggers retransformation of classes via this file's Transform method + extern "C" JNIEXPORT jint JNICALL + Java_com_android_dx_mockito_inline_tests_MultipleJvmtiAgentsInterference_nativeRetransformClasses( + JNIEnv *env, + jobject thiz, + jobjectArray classes) { + jsize numTransformedClasses = env->GetArrayLength(classes); + jclass *transformedClasses = (jclass *) malloc(numTransformedClasses * sizeof(jclass)); + for (int i = 0; i < numTransformedClasses; i++) { + transformedClasses[i] = (jclass) env->NewGlobalRef(env->GetObjectArrayElement(classes, + i)); + } + + jvmtiError error = localJvmtiEnv->RetransformClasses(numTransformedClasses, + transformedClasses); + + for (int i = 0; i < numTransformedClasses; i++) { + env->DeleteGlobalRef(transformedClasses[i]); + } + free(transformedClasses); + + return error; + } +}
\ No newline at end of file diff --git a/dexmaker-mockito-inline/CMakeLists.txt b/dexmaker-mockito-inline/CMakeLists.txt new file mode 100644 index 0000000..cd26d58 --- /dev/null +++ b/dexmaker-mockito-inline/CMakeLists.txt @@ -0,0 +1,33 @@ +cmake_minimum_required(VERSION 3.4.1) + +set(slicer_sources + external/slicer/bytecode_encoder.cc + external/slicer/code_ir.cc + external/slicer/common.cc + external/slicer/control_flow_graph.cc + external/slicer/debuginfo_encoder.cc + external/slicer/dex_bytecode.cc + external/slicer/dex_format.cc + external/slicer/dex_ir_builder.cc + external/slicer/dex_ir.cc + external/slicer/dex_utf8.cc + external/slicer/instrumentation.cc + external/slicer/reader.cc + external/slicer/tryblocks_encoder.cc + external/slicer/writer.cc) + +add_library(slicer + STATIC + ${slicer_sources}) + +include_directories(external/jdk external/slicer/) + +target_link_libraries(slicer z) + +add_library(dexmakerjvmtiagent + SHARED + src/main/jni/dexmakerjvmtiagent/agent.cc) + +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -frtti -Wall -Werror -Wno-unused-parameter -Wno-shift-count-overflow -Wno-error=non-virtual-dtor -Wno-sign-compare -Wno-switch -Wno-missing-braces") + +target_link_libraries(dexmakerjvmtiagent slicer) diff --git a/dexmaker-mockito-inline/build.gradle b/dexmaker-mockito-inline/build.gradle new file mode 100644 index 0000000..54e85ec --- /dev/null +++ b/dexmaker-mockito-inline/build.gradle @@ -0,0 +1,33 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion 25 + buildToolsVersion "25.0.0" + + lintOptions { + abortOnError false + } + + defaultConfig { + minSdkVersion 25 + targetSdkVersion 25 + versionName VERSION_NAME + } + + externalNativeBuild { + cmake { + path 'CMakeLists.txt' + } + } + +} + +repositories { + jcenter() +} + +dependencies { + compile project(':dexmaker') + compile 'org.mockito:mockito-core:2.12.0' +} + diff --git a/dexmaker-mockito-inline/src/main/AndroidManifest.xml b/dexmaker-mockito-inline/src/main/AndroidManifest.xml new file mode 100644 index 0000000..9295bf2 --- /dev/null +++ b/dexmaker-mockito-inline/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ +<manifest package="com.android.dx.mockito.inline"> + <application /> +</manifest> diff --git a/dexmaker-mockito-inline/src/main/java/com/android/dx/mockito/inline/ClassTransformer.java b/dexmaker-mockito-inline/src/main/java/com/android/dx/mockito/inline/ClassTransformer.java new file mode 100644 index 0000000..f719304 --- /dev/null +++ b/dexmaker-mockito-inline/src/main/java/com/android/dx/mockito/inline/ClassTransformer.java @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2017 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.mockito.inline; + +import org.mockito.exceptions.base.MockitoException; +import org.mockito.internal.util.concurrent.WeakConcurrentMap; +import org.mockito.internal.util.concurrent.WeakConcurrentSet; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.security.ProtectionDomain; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Random; +import java.util.Set; + +/** + * Adds entry hooks (that eventually call into + * {@link MockMethodAdvice#handle(Object, Method, Object[])} to all non-abstract methods of the + * supplied classes. + * + * <p></p>Transforming a class to adding entry hooks follow the following simple steps: + * <ol> + * <li>{@link #mockClass(MockFeatures)}</li> + * <li>{@link JvmtiAgent#requestTransformClasses(Class[])}</li> + * <li>{@link JvmtiAgent#nativeRetransformClasses(Class[])}</li> + * <li>agent.cc::Transform</li> + * <li>{@link JvmtiAgent#runTransformers(ClassLoader, String, Class, ProtectionDomain, byte[])}</li> + * <li>{@link #transform(Class, byte[])}</li> + * <li>{@link #nativeRedefine(String, byte[])}</li> + * </ol> + * + */ +class ClassTransformer { + // Some classes are so deeply optimized inside the runtime that they cannot be transformed + private static final Set<Class<? extends java.io.Serializable>> EXCLUDES = new HashSet<>( + Arrays.asList(Class.class, + Boolean.class, + Byte.class, + Short.class, + Character.class, + Integer.class, + Long.class, + Float.class, + Double.class, + String.class)); + private final static Random random = new Random(); + + /** Jvmti agent responsible for triggering transformation s*/ + private final JvmtiAgent agent; + + /** Types that have already be transformed */ + private final WeakConcurrentSet<Class<?>> mockedTypes; + + /** + * A unique identifier that is baked into the transformed classes. The entry hooks will then + * pass this identifier to + * {@code com.android.dx.mockito.inline.MockMethodDispatcher#get(String, Object)} to + * find the advice responsible for handling the method call interceptions. + */ + private final String identifier; + + /** + * We can only have a single transformation going on at a time, hence synchronize the + * transformation process via this lock. + * + * @see #mockClass(MockFeatures) + */ + private final static Object lock = new Object(); + + /** + * Create a new generator. + * + * Creating more than one generator might cause transformations to overwrite each other. + * + * @param agent agent used to trigger transformations + * @param dispatcherClass {@code com.android.dx.mockito.inline.MockMethodDispatcher} + * that will dispatch method calls that might need to get intercepted. + * @param mocks list of mocked objects. As all objects of a class use the same transformed + * bytecode the {@link MockMethodAdvice} needs to check this list if a object is + * mocked or not. + */ + ClassTransformer(JvmtiAgent agent, Class dispatcherClass, + WeakConcurrentMap<Object, InvocationHandlerAdapter> mocks) { + this.agent = agent; + mockedTypes = new WeakConcurrentSet<>(WeakConcurrentSet.Cleaner.INLINE); + identifier = Long.toString(random.nextLong()); + MockMethodAdvice advice = new MockMethodAdvice(mocks); + + try { + dispatcherClass.getMethod("set", String.class, Object.class).invoke(null, identifier, + advice); + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + throw new IllegalStateException(e); + } + + agent.addTransformer(this); + } + + /** + * Trigger the process to add entry hooks to a class (and all its parents). + * + * @param features specification what to mock + */ + <T> void mockClass(MockFeatures<T> features) { + boolean subclassingRequired = !features.interfaces.isEmpty() + || Modifier.isAbstract(features.mockedType.getModifiers()); + + if (subclassingRequired + && !features.mockedType.isArray() + && !features.mockedType.isPrimitive() + && Modifier.isFinal(features.mockedType.getModifiers())) { + throw new MockitoException("Unsupported settings with this type '" + + features.mockedType.getName() + "'"); + } + + synchronized (lock) { + Set<Class<?>> types = new HashSet<>(); + Class<?> type = features.mockedType; + + do { + boolean wasAdded = mockedTypes.add(type); + + if (wasAdded) { + if (!EXCLUDES.contains(type)) { + types.add(type); + } + + type = type.getSuperclass(); + } else { + break; + } + } while (type != null && !type.isInterface()); + + if (!types.isEmpty()) { + try { + agent.requestTransformClasses(types.toArray(new Class<?>[types.size()])); + } catch (UnmodifiableClassException exception) { + for (Class<?> failed : types) { + mockedTypes.remove(failed); + } + + throw new MockitoException("Could not modify all classes " + types, exception); + } + } + } + } + + /** + * Add entry hooks to all methods of a class. + * + * <p>Called by the agent after triggering the transformation via + * {@link #mockClass(MockFeatures)}. + * + * @param classBeingRedefined class the hooks should be added to + * @param classfileBuffer original byte code of the class + * + * @return transformed class + */ + byte[] transform(Class<?> classBeingRedefined, byte[] classfileBuffer) throws + IllegalClassFormatException { + if (classBeingRedefined == null + || !mockedTypes.contains(classBeingRedefined)) { + return null; + } else { + try { + return nativeRedefine(identifier, classfileBuffer); + } catch (Throwable throwable) { + throw new IllegalClassFormatException(); + } + } + } + + private native byte[] nativeRedefine(String identifier, byte[] original); +} diff --git a/dexmaker-mockito-inline/src/main/java/com/android/dx/mockito/inline/DexmakerStackTraceCleaner.java b/dexmaker-mockito-inline/src/main/java/com/android/dx/mockito/inline/DexmakerStackTraceCleaner.java new file mode 100644 index 0000000..2757a9e --- /dev/null +++ b/dexmaker-mockito-inline/src/main/java/com/android/dx/mockito/inline/DexmakerStackTraceCleaner.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2017 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.mockito.inline; + +import org.mockito.exceptions.stacktrace.StackTraceCleaner; +import org.mockito.plugins.StackTraceCleanerProvider; + +/** + * Cleans out mockito internal elements out of stack traces. This creates stack traces as if mockito + * would have not intercepted any calls. + */ +public final class DexmakerStackTraceCleaner implements StackTraceCleanerProvider { + @Override + public StackTraceCleaner getStackTraceCleaner(final StackTraceCleaner defaultCleaner) { + return new StackTraceCleaner() { + @Override + public boolean isIn(StackTraceElement candidate) { + String className = candidate.getClassName(); + + return defaultCleaner.isIn(candidate) + // dexmaker class proxies + && !className.endsWith("_Proxy") + + && !className.startsWith("java.lang.reflect.Method") + && !className.startsWith("java.lang.reflect.Proxy") + && !(className.startsWith("com.android.dx.mockito.") + // Do not clean unit tests + && !className.startsWith("com.android.dx.mockito.inline.tests")) + + // dalvik interface proxies + && !className.startsWith("$Proxy") + && !className.matches(".*\\.\\$Proxy[\\d]+"); + } + }; + } + +} diff --git a/dexmaker-mockito-inline/src/main/java/com/android/dx/mockito/inline/IllegalClassFormatException.java b/dexmaker-mockito-inline/src/main/java/com/android/dx/mockito/inline/IllegalClassFormatException.java new file mode 100644 index 0000000..8da7b2b --- /dev/null +++ b/dexmaker-mockito-inline/src/main/java/com/android/dx/mockito/inline/IllegalClassFormatException.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2017 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.mockito.inline; + +/** + * Exception thrown if a class cannot be transformed by + * {@link ClassTransformer#transform(Class, byte[])} + */ +class IllegalClassFormatException extends Exception { +} diff --git a/dexmaker-mockito-inline/src/main/java/com/android/dx/mockito/inline/InlineDexmakerMockMaker.java b/dexmaker-mockito-inline/src/main/java/com/android/dx/mockito/inline/InlineDexmakerMockMaker.java new file mode 100644 index 0000000..d5be235 --- /dev/null +++ b/dexmaker-mockito-inline/src/main/java/com/android/dx/mockito/inline/InlineDexmakerMockMaker.java @@ -0,0 +1,302 @@ +/* + * Copyright (C) 2017 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.mockito.inline; + +import com.android.dx.stock.ProxyBuilder; +import com.android.dx.stock.ProxyBuilder.MethodSetEntry; + +import org.mockito.exceptions.base.MockitoException; +import org.mockito.internal.configuration.plugins.Plugins; +import org.mockito.internal.creation.instance.Instantiator; +import org.mockito.internal.util.Platform; +import org.mockito.internal.util.concurrent.WeakConcurrentMap; +import org.mockito.invocation.MockHandler; +import org.mockito.mock.MockCreationSettings; +import org.mockito.plugins.MockMaker; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Proxy; +import java.util.HashSet; +import java.util.Set; + +/** + * Generates mock instances on Android's runtime that can mock final methods. + * + * <p>This is done by transforming the byte code of the classes to add method entry hooks. + */ +public final class InlineDexmakerMockMaker implements MockMaker { + private static final String DISPATCHER_CLASS_NAME = + "com.android.dx.mockito.inline.MockMethodDispatcher"; + private static final String DISPATCHER_JAR = "dispatcher.jar"; + + /** {@link com.android.dx.mockito.inline.JvmtiAgent} set up during one time init */ + private static final JvmtiAgent AGENT; + + /** Error during one time init or {@code null} if init was successful*/ + private static final Throwable INITIALIZATION_ERROR; + + /** + * Class injected into the bootstrap classloader. All entry hooks added to methods will call + * this class. + */ + private static final Class DISPATCHER_CLASS; + + /* + * One time setup to allow the system to mocking via this mock maker. + */ + static { + JvmtiAgent agent; + Throwable initializationError = null; + Class dispatcherClass = null; + try { + try { + agent = new JvmtiAgent(); + + try (InputStream is = InlineDexmakerMockMaker.class.getClassLoader() + .getResource(DISPATCHER_JAR).openStream()) { + agent.appendToBootstrapClassLoaderSearch(is); + } + + try { + dispatcherClass = Class.forName(DISPATCHER_CLASS_NAME, true, + Object.class.getClassLoader()); + + if (dispatcherClass == null) { + throw new IllegalStateException(DISPATCHER_CLASS_NAME + + " could not be loaded"); + } + } catch (ClassNotFoundException cnfe) { + throw new IllegalStateException( + "Mockito failed to inject the MockMethodDispatcher class into the " + + "bootstrap class loader\n\nIt seems like your current VM does not " + + "support the jvmti API correctly.", cnfe); + } + } catch (IOException ioe) { + throw new IllegalStateException( + "Mockito could not self-attach a jvmti agent to the current VM. This " + + "feature is required for inline mocking.\nThis error occured due to an " + + "I/O error during the creation of this agent: " + ioe + "\n\n" + + "Potentially, the current VM does not support the jvmti API correctly", + ioe); + } + } catch (Throwable throwable) { + agent = null; + initializationError = throwable; + } + + AGENT = agent; + INITIALIZATION_ERROR = initializationError; + DISPATCHER_CLASS = dispatcherClass; + } + + /** + * All currently active mocks. We modify the class's byte code. Some objects of the class are + * modified, some are not. This list helps the {@link MockMethodAdvice} help figure out if a + * object's method calls should be intercepted. + */ + private final WeakConcurrentMap<Object, InvocationHandlerAdapter> mocks; + + /** + * Class doing the actual byte code transformation. + */ + private final ClassTransformer classTransformer; + + /** + * Create a new mock maker. + */ + public InlineDexmakerMockMaker() { + if (INITIALIZATION_ERROR != null) { + throw new RuntimeException( + "Could not initialize inline mock maker.\n" + + "\n" + + Platform.describe(), INITIALIZATION_ERROR); + } + + mocks = new WeakConcurrentMap.WithInlinedExpunction<>(); + classTransformer = new ClassTransformer(AGENT, DISPATCHER_CLASS, mocks); + } + + /** + * Get methods to proxy. + * + * <p>Only abstract methods will need to get proxied as all other methods will get an entry + * hook. + * + * @param settings description of the current mocking process. + * + * @return methods to proxy. + */ + private <T> Method[] getMethodsToProxy(MockCreationSettings<T> settings) { + Set<MethodSetEntry> abstractMethods = new HashSet<>(); + Set<MethodSetEntry> nonAbstractMethods = new HashSet<>(); + + Class<?> superClass = settings.getTypeToMock(); + while (superClass != null) { + for (Method method : superClass.getDeclaredMethods()) { + if (Modifier.isAbstract(method.getModifiers()) + && !nonAbstractMethods.contains(new MethodSetEntry(method))) { + abstractMethods.add(new MethodSetEntry(method)); + } else { + nonAbstractMethods.add(new MethodSetEntry(method)); + } + } + + superClass = superClass.getSuperclass(); + } + + for (Class<?> i : settings.getTypeToMock().getInterfaces()) { + for (Method method : i.getMethods()) { + if (!nonAbstractMethods.contains(new MethodSetEntry(method))) { + abstractMethods.add(new MethodSetEntry(method)); + } + } + } + + for (Class<?> i : settings.getExtraInterfaces()) { + for (Method method : i.getMethods()) { + if (!nonAbstractMethods.contains(new MethodSetEntry(method))) { + abstractMethods.add(new MethodSetEntry(method)); + } + } + } + + Method[] methodsToProxy = new Method[abstractMethods.size()]; + int i = 0; + for (MethodSetEntry entry : abstractMethods) { + methodsToProxy[i++] = entry.originalMethod; + } + + return methodsToProxy; + } + + @Override + public <T> T createMock(MockCreationSettings<T> settings, MockHandler handler) { + Class<T> typeToMock = settings.getTypeToMock(); + Set<Class<?>> interfacesSet = settings.getExtraInterfaces(); + Class<?>[] extraInterfaces = interfacesSet.toArray(new Class[interfacesSet.size()]); + InvocationHandlerAdapter handlerAdapter = new InvocationHandlerAdapter(handler); + + T mock; + if (typeToMock.isInterface()) { + // support interfaces via java.lang.reflect.Proxy + Class[] classesToMock = new Class[extraInterfaces.length + 1]; + classesToMock[0] = typeToMock; + System.arraycopy(extraInterfaces, 0, classesToMock, 1, extraInterfaces.length); + + // newProxyInstance returns the type of typeToMock + mock = (T) Proxy.newProxyInstance(typeToMock.getClassLoader(), classesToMock, + handlerAdapter); + } else { + boolean subclassingRequired = !interfacesSet.isEmpty() + || Modifier.isAbstract(typeToMock.getModifiers()); + + // Add entry hooks to non-abstract methods. + classTransformer.mockClass(MockFeatures.withMockFeatures(typeToMock, interfacesSet)); + + Class<? extends T> proxyClass; + + Instantiator instantiator = Plugins.getInstantiatorProvider().getInstantiator(settings); + + if (subclassingRequired) { + try { + // support abstract methods via dexmaker's ProxyBuilder + proxyClass = ProxyBuilder.forClass(typeToMock).implementing(extraInterfaces) + .onlyMethods(getMethodsToProxy(settings)).withSharedClassLoader() + .buildProxyClass(); + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new MockitoException("Failed to mock " + typeToMock, e); + } + + try { + mock = instantiator.newInstance(proxyClass); + } catch (org.mockito.internal.creation.instance.InstantiationException e) { + throw new MockitoException("Unable to create mock instance of type '" + + proxyClass.getSuperclass().getSimpleName() + "'", e); + } + + ProxyBuilder.setInvocationHandler(mock, handlerAdapter); + } else { + try { + mock = instantiator.newInstance(typeToMock); + } catch (org.mockito.internal.creation.instance.InstantiationException e) { + throw new MockitoException("Unable to create mock instance of type '" + + typeToMock.getSimpleName() + "'", e); + } + } + } + + mocks.put(mock, handlerAdapter); + return mock; + } + + @Override + public void resetMock(Object mock, MockHandler newHandler, MockCreationSettings settings) { + InvocationHandlerAdapter adapter = getInvocationHandlerAdapter(mock); + if (adapter != null) { + adapter.setHandler(newHandler); + } + } + + @Override + public TypeMockability isTypeMockable(final Class<?> type) { + return new TypeMockability() { + @Override + public boolean mockable() { + return !type.isPrimitive() && type != String.class; + } + + @Override + public String nonMockableReason() { + if (type.isPrimitive()) { + return "primitive type"; + } + + if (type == String.class) { + return "string"; + } + + return "not handled type"; + } + }; + } + + @Override + public MockHandler getHandler(Object mock) { + InvocationHandlerAdapter adapter = getInvocationHandlerAdapter(mock); + return adapter != null ? adapter.getHandler() : null; + } + + /** + * Get the {@link InvocationHandlerAdapter} registered for a mock. + * + * @param instance instance that might be mocked + * + * @return adapter for this mock, or {@code null} if instance is not mocked + */ + private InvocationHandlerAdapter getInvocationHandlerAdapter(Object instance) { + if (instance == null) { + return null; + } + + return mocks.get(instance); + } +} diff --git a/dexmaker-mockito-inline/src/main/java/com/android/dx/mockito/inline/InterceptedInvocation.java b/dexmaker-mockito-inline/src/main/java/com/android/dx/mockito/inline/InterceptedInvocation.java new file mode 100644 index 0000000..a6d11b5 --- /dev/null +++ b/dexmaker-mockito-inline/src/main/java/com/android/dx/mockito/inline/InterceptedInvocation.java @@ -0,0 +1,189 @@ +/* + * Copyright (c) 2016 Mockito contributors + * This program is made available under the terms of the MIT License. + */ + +package com.android.dx.mockito.inline; + +import org.mockito.internal.debugging.LocationImpl; +import org.mockito.internal.exceptions.VerificationAwareInvocation; +import org.mockito.internal.invocation.ArgumentsProcessor; +import org.mockito.internal.invocation.MockitoMethod; +import org.mockito.internal.reporting.PrintSettings; +import org.mockito.invocation.Invocation; +import org.mockito.invocation.Location; +import org.mockito.invocation.StubInfo; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.Arrays; + +import static org.mockito.internal.exceptions.Reporter.cannotCallAbstractRealMethod; + +/** + * {@link Invocation} used when intercepting methods from an method entry hook. + */ +class InterceptedInvocation implements Invocation, VerificationAwareInvocation { + /** The mocked instance */ + private final Object mock; + + /** The method invoked */ + private final MockitoMethod method; + + /** expanded arguments to the method */ + private final Object[] arguments; + + /** raw arguments to the method */ + private final Object[] rawArguments; + + /** The super method */ + private final SuperMethod superMethod; + + /** sequence number of the invocation (different for each invocation) */ + private final int sequenceNumber; + + /** the location of the invocation (i.e. the stack trace) */ + private final Location location; + + /** Was this invocation {@link #markVerified() marked as verified} */ + private boolean verified; + + /** Should this be {@link #ignoreForVerification()} ignored for verification?} */ + private boolean isIgnoredForVerification; + + /** The stubinfo is this was {@link #markStubbed(StubInfo) markes as stubbed}*/ + private StubInfo stubInfo; + + /** + * Create a new invocation. + * + * @param mock mocked instance + * @param method method invoked + * @param arguments arguments to the method + * @param superMethod super method + * @param sequenceNumber sequence number of the invocation + */ + InterceptedInvocation(Object mock, MockitoMethod method, Object[] arguments, + SuperMethod superMethod, int sequenceNumber) { + this.mock = mock; + this.method = method; + this.arguments = ArgumentsProcessor.expandArgs(method, arguments); + this.rawArguments = arguments; + this.superMethod = superMethod; + this.sequenceNumber = sequenceNumber; + location = new LocationImpl(); + } + + @Override + public boolean isVerified() { + return verified || isIgnoredForVerification; + } + + @Override + public int getSequenceNumber() { + return sequenceNumber; + } + + @Override + public Location getLocation() { + return location; + } + + @Override + public Object[] getRawArguments() { + return rawArguments; + } + + @Override + public Class<?> getRawReturnType() { + return method.getReturnType(); + } + + @Override + public void markVerified() { + verified = true; + } + + @Override + public StubInfo stubInfo() { + return stubInfo; + } + + @Override + public void markStubbed(StubInfo stubInfo) { + this.stubInfo = stubInfo; + } + + @Override + public boolean isIgnoredForVerification() { + return isIgnoredForVerification; + } + + @Override + public void ignoreForVerification() { + isIgnoredForVerification = true; + } + + @Override + public Object getMock() { + return mock; + } + + @Override + public Method getMethod() { + return method.getJavaMethod(); + } + + @Override + public Object[] getArguments() { + return arguments; + } + + @Override + @SuppressWarnings("unchecked") + public <T> T getArgument(int index) { + return (T) arguments[index]; + } + + @Override + public Object callRealMethod() throws Throwable { + if (!superMethod.isInvokable()) { + throw cannotCallAbstractRealMethod(); + } + return superMethod.invoke(); + } + + @Override + public int hashCode() { + // TODO SF we need to provide hash code implementation so that there are no unexpected, + // slight perf issues + return 1; + } + + @Override + public boolean equals(Object o) { + if (o == null || !o.getClass().equals(this.getClass())) { + return false; + } + InterceptedInvocation other = (InterceptedInvocation) o; + return this.mock.equals(other.mock) + && this.method.equals(other.method) + && this.equalArguments(other.arguments); + } + + private boolean equalArguments(Object[] arguments) { + return Arrays.equals(arguments, this.arguments); + } + + @Override + public String toString() { + return new PrintSettings().print(ArgumentsProcessor.argumentsToMatchers(getArguments()), + this); + } + + interface SuperMethod extends Serializable { + boolean isInvokable(); + + Object invoke() throws Throwable; + } +} diff --git a/dexmaker-mockito-inline/src/main/java/com/android/dx/mockito/inline/InvocationHandlerAdapter.java b/dexmaker-mockito-inline/src/main/java/com/android/dx/mockito/inline/InvocationHandlerAdapter.java new file mode 100644 index 0000000..0c9af71 --- /dev/null +++ b/dexmaker-mockito-inline/src/main/java/com/android/dx/mockito/inline/InvocationHandlerAdapter.java @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2012 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.mockito.inline; + +import org.mockito.internal.creation.DelegatingMethod; +import org.mockito.internal.debugging.LocationImpl; +import org.mockito.internal.invocation.ArgumentsProcessor; +import org.mockito.internal.progress.SequenceNumber; +import org.mockito.invocation.Invocation; +import org.mockito.invocation.Location; +import org.mockito.invocation.MockHandler; +import org.mockito.invocation.StubInfo; +import org.mockito.mock.MockCreationSettings; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +import static org.mockito.internal.exceptions.Reporter.cannotCallAbstractRealMethod; + +/** + * Handles proxy and entry hook method invocations added by + * {@link InlineDexmakerMockMaker#createMock(MockCreationSettings, MockHandler)} + */ +final class InvocationHandlerAdapter implements InvocationHandler { + private MockHandler handler; + + InvocationHandlerAdapter(MockHandler handler) { + this.handler = handler; + } + + private static boolean isEqualsMethod(Method method) { + return method.getName().equals("equals") + && method.getParameterTypes().length == 1 + && method.getParameterTypes()[0] == Object.class; + } + + private static boolean isHashCodeMethod(Method method) { + return method.getName().equals("hashCode") + && method.getParameterTypes().length == 0; + } + + /** + * Intercept a method call. Called <u>before</u> a method is called by the method entry hook. + * + * <p>This does the same as {@link #invoke(Object, Method, Object[])} but this handles methods + * that got and entry hook. + * + * @param mock mocked object + * @param method method that was called + * @param args arguments to the method + * @param superMethod The super method + * + * @return mocked result + * @throws Throwable An exception if thrown + */ + Object interceptEntryHook(Object mock, Method method, Object[] args, + InterceptedInvocation.SuperMethod superMethod) throws Throwable { + return handler.handle(new InterceptedInvocation(mock, new DelegatingMethod(method), args, + superMethod, SequenceNumber.next())); + } + + /** + * Intercept a method call. Called <u>before</u> a method is called by the proxied method. + * + * <p>This does the same as {@link #interceptEntryHook(Object, Method, Object[], + * InterceptedInvocation.SuperMethod)} but this handles proxied methods. We only proxy abstract + * methods. + * + * @param proxy proxies object + * @param method method that was called + * @param argsIn arguments to the method + * + * @return mocked result + * @throws Throwable An exception if thrown + */ + @Override + public Object invoke(final Object proxy, final Method method, Object[] argsIn) throws + Throwable { + // args can be null if the method invoked has no arguments, but Mockito expects a non-null + // array + final Object[] args = argsIn != null ? argsIn : new Object[0]; + if (isEqualsMethod(method)) { + return proxy == args[0]; + } else if (isHashCodeMethod(method)) { + return System.identityHashCode(proxy); + } + + return handler.handle(new ProxyInvocation(proxy, method, args, new DelegatingMethod + (method), SequenceNumber.next(), new LocationImpl())); + } + + /** + * Get the handler registered with this adapter. + * + * @return handler + */ + MockHandler getHandler() { + return handler; + } + + /** + * Set a new handler for this adapter. + */ + void setHandler(MockHandler handler) { + this.handler = handler; + } + + /** + * Invocation on a proxy + */ + private class ProxyInvocation implements Invocation { + private final Object proxy; + private final Method method; + private final Object[] rawArgs; + private final int sequenceNumber; + private final Location location; + private final Object[] args; + + private StubInfo stubInfo; + private boolean isIgnoredForVerification; + private boolean verified; + + private ProxyInvocation(Object proxy, Method method, Object[] rawArgs, DelegatingMethod + mockitoMethod, int sequenceNumber, Location location) { + this.rawArgs = rawArgs; + this.proxy = proxy; + this.method = method; + this.sequenceNumber = sequenceNumber; + this.location = location; + args = ArgumentsProcessor.expandArgs(mockitoMethod, rawArgs); + } + + @Override + public Object getMock() { + return proxy; + } + + @Override + public Method getMethod() { + return method; + } + + @Override + public Object[] getArguments() { + return args; + } + + @Override + public <T> T getArgument(int index) { + return (T)args[index]; + } + + @Override + public Object callRealMethod() throws Throwable { + if (Modifier.isAbstract(method.getModifiers())) { + throw cannotCallAbstractRealMethod(); + } + return method.invoke(proxy, rawArgs); + } + + @Override + public boolean isVerified() { + return verified || isIgnoredForVerification; + } + + @Override + public int getSequenceNumber() { + return sequenceNumber; + } + + @Override + public Location getLocation() { + return location; + } + + @Override + public Object[] getRawArguments() { + return rawArgs; + } + + @Override + public Class<?> getRawReturnType() { + return method.getReturnType(); + } + + @Override + public void markVerified() { + verified = true; + } + + @Override + public StubInfo stubInfo() { + return stubInfo; + } + + @Override + public void markStubbed(StubInfo stubInfo) { + this.stubInfo = stubInfo; + } + + @Override + public boolean isIgnoredForVerification() { + return isIgnoredForVerification; + } + + @Override + public void ignoreForVerification() { + isIgnoredForVerification = true; + } + } +} diff --git a/dexmaker-mockito-inline/src/main/java/com/android/dx/mockito/inline/JvmtiAgent.java b/dexmaker-mockito-inline/src/main/java/com/android/dx/mockito/inline/JvmtiAgent.java new file mode 100644 index 0000000..1b4a550 --- /dev/null +++ b/dexmaker-mockito-inline/src/main/java/com/android/dx/mockito/inline/JvmtiAgent.java @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2017 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.mockito.inline; + +import android.os.Build; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.InvocationTargetException; +import java.security.ProtectionDomain; +import java.util.ArrayList; + +import dalvik.system.BaseDexClassLoader; + +/** + * Interface to the native jvmti agent in agent.cc + */ +class JvmtiAgent { + private static final String AGENT_LIB_NAME = "dexmakerjvmtiagent"; + + private static final Object lock = new Object(); + + /** Registered byte code transformers */ + private final ArrayList<ClassTransformer> transformers = new ArrayList<>(); + + private native void nativeRegisterTransformerHook(); + + /** + * Enable jvmti and load agent. + * + * <p><b>If there are more than agent transforming classes the other agent might remove + * transformations added by this agent.</b> + * + * @throws IOException If jvmti could not be enabled or agent could not be loaded + */ + JvmtiAgent() throws IOException { + // TODO (moltmann@google.com): Replace with proper check for >= P + if (!Build.VERSION.CODENAME.equals("P")) { + throw new IOException("Requires Android P. Build is " + Build.VERSION.CODENAME); + } + + Throwable loadJvmtiException = null; + + ClassLoader cl = JvmtiAgent.class.getClassLoader(); + if (!(cl instanceof BaseDexClassLoader)) { + throw new IOException("Could not load jvmti plugin as JvmtiAgent class was not loaded " + + "by a BaseDexClassLoader"); + } + + // Currently Debug.attachJvmtiAgent requires a file in the right directory + File copiedAgent = File.createTempFile("agent", ".so"); + copiedAgent.deleteOnExit(); + + try (InputStream is = new FileInputStream( + ((BaseDexClassLoader) cl).findLibrary(AGENT_LIB_NAME))) { + try (OutputStream os = new FileOutputStream(copiedAgent)) { + byte[] buffer = new byte[64 * 1024]; + + while (true) { + int numRead = is.read(buffer); + if (numRead == -1) { + break; + } + os.write(buffer, 0, numRead); + } + } + } + + try { + /* + * TODO (moltmann@google.com): Replace with regular method call once the API becomes + * public + */ + Class.forName("android.os.Debug").getMethod("attachJvmtiAgent", String.class, + String.class).invoke(null, copiedAgent.getAbsolutePath(), null); + } catch (InvocationTargetException e) { + loadJvmtiException = e.getCause(); + } catch (IllegalAccessException | ClassNotFoundException | NoSuchMethodException e) { + loadJvmtiException = e; + } + + if (loadJvmtiException != null) { + if (loadJvmtiException instanceof IOException) { + throw (IOException)loadJvmtiException; + } else { + throw new IOException("Could not load jvmti plugin", + loadJvmtiException); + } + } + + nativeRegisterTransformerHook(); + } + + private native void nativeUnregisterTransformerHook(); + + @Override + protected void finalize() throws Throwable { + nativeUnregisterTransformerHook(); + } + + private native static void nativeAppendToBootstrapClassLoaderSearch(String absolutePath); + + /** + * Append the jar to be bootstrap class load. This makes the classes in the jar behave as if + * they are loaded from the BCL. E.g. classes from java.lang can now call the classes in the + * jar. + * + * @param jarStream stream of jar to be added + */ + void appendToBootstrapClassLoaderSearch(InputStream jarStream) throws IOException { + File jarFile = File.createTempFile("mockito-boot", ".jar"); + jarFile.deleteOnExit(); + + byte[] buffer = new byte[64 * 1024]; + try (OutputStream os = new FileOutputStream(jarFile)) { + while (true) { + int numRead = jarStream.read(buffer); + if (numRead == -1) { + break; + } + + os.write(buffer, 0, numRead); + } + } + + nativeAppendToBootstrapClassLoaderSearch(jarFile.getAbsolutePath()); + } + + /** + * Ask the agent to trigger transformation of some classes. This will extract the byte code of + * the classes and the call back the {@link #addTransformer(ClassTransformer) transformers} for + * each individual class. + * + * @param classes The classes to transform + * + * @throws UnmodifiableClassException If one of the classes can not be transformed + */ + void requestTransformClasses(Class<?>[] classes) throws UnmodifiableClassException { + synchronized (lock) { + try { + nativeRetransformClasses(classes); + } catch (RuntimeException e) { + throw new UnmodifiableClassException(e); + } + } + } + + /** + * Register a transformer. These are called for each class when a transformation was triggered + * via {@link #requestTransformClasses(Class[])}. + * + * @param transformer the transformer to add. + */ + void addTransformer(ClassTransformer transformer) { + transformers.add(transformer); + } + + // called by JNI + @SuppressWarnings("unused") + public byte[] runTransformers(ClassLoader loader, String className, + Class<?> classBeingRedefined, ProtectionDomain protectionDomain, + byte[] classfileBuffer) throws IllegalClassFormatException { + byte[] transformedByteCode = classfileBuffer; + for (ClassTransformer transformer : transformers) { + transformedByteCode = transformer.transform(classBeingRedefined, transformedByteCode); + } + + return transformedByteCode; + } + + private native void nativeRetransformClasses(Class<?>[] classes); +} diff --git a/dexmaker-mockito-inline/src/main/java/com/android/dx/mockito/inline/MockFeatures.java b/dexmaker-mockito-inline/src/main/java/com/android/dx/mockito/inline/MockFeatures.java new file mode 100644 index 0000000..01d01bf --- /dev/null +++ b/dexmaker-mockito-inline/src/main/java/com/android/dx/mockito/inline/MockFeatures.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2016 Mockito contributors + * This program is made available under the terms of the MIT License. + */ + +package com.android.dx.mockito.inline; + +import java.util.Collections; +import java.util.Set; + +class MockFeatures<T> { + final Class<T> mockedType; + final Set<Class<?>> interfaces; + + private MockFeatures(Class<T> mockedType, Set<Class<?>> interfaces) { + this.mockedType = mockedType; + this.interfaces = Collections.unmodifiableSet(interfaces); + } + + static <T> MockFeatures<T> withMockFeatures(Class<T> mockedType, Set<Class<?>> interfaces) { + return new MockFeatures<>(mockedType, interfaces); + } +} diff --git a/dexmaker-mockito-inline/src/main/java/com/android/dx/mockito/inline/MockMethodAdvice.java b/dexmaker-mockito-inline/src/main/java/com/android/dx/mockito/inline/MockMethodAdvice.java new file mode 100644 index 0000000..4cf2ac8 --- /dev/null +++ b/dexmaker-mockito-inline/src/main/java/com/android/dx/mockito/inline/MockMethodAdvice.java @@ -0,0 +1,341 @@ +/* + * Copyright (c) 2016 Mockito contributors + * This program is made available under the terms of the MIT License. + */ + +package com.android.dx.mockito.inline; + +import org.mockito.internal.exceptions.stacktrace.ConditionalStackTraceFilter; +import org.mockito.internal.util.concurrent.WeakConcurrentMap; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.concurrent.Callable; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Backend for the method entry hooks. Checks if the hooks should cause an interception or should + * be ignored. + */ +class MockMethodAdvice { + private final WeakConcurrentMap<Object, InvocationHandlerAdapter> interceptors; + + /** Pattern to decompose a instrumentedMethodWithTypeAndSignature */ + private final Pattern methodPattern = Pattern.compile("(.*)#(.*)\\((.*)\\)"); + + private final SelfCallInfo selfCallInfo = new SelfCallInfo(); + + MockMethodAdvice(WeakConcurrentMap<Object, InvocationHandlerAdapter> interceptors) { + this.interceptors = interceptors; + } + + /** + * Try to invoke the method {@code origin} on {@code instance}. + * + * @param origin method to invoke + * @param instance instance to invoke the method on. + * @param arguments arguments to the method + * + * @return result of the method + * + * @throws Throwable Exception if thrown by the method + */ + private static Object tryInvoke(Method origin, Object instance, Object[] arguments) + throws Throwable { + try { + return origin.invoke(instance, arguments); + } catch (InvocationTargetException exception) { + Throwable cause = exception.getCause(); + + new ConditionalStackTraceFilter().filter(hideRecursiveCall(cause, + new Throwable().getStackTrace().length, origin.getDeclaringClass())); + + throw cause; + } + } + + /** + * Remove calls to a class from a throwable's stack. + * + * @param throwable throwable to clean + * @param current stack frame number to start cleaning from (upwards) + * @param targetType class to remove from the stack + * + * @return throwable with the cleaned stack + */ + private static Throwable hideRecursiveCall(Throwable throwable, int current, + Class<?> targetType) { + try { + StackTraceElement[] stack = throwable.getStackTrace(); + int skip = 0; + StackTraceElement next; + + do { + next = stack[stack.length - current - ++skip]; + } while (!next.getClassName().equals(targetType.getName())); + + int top = stack.length - current - skip; + StackTraceElement[] cleared = new StackTraceElement[stack.length - skip]; + System.arraycopy(stack, 0, cleared, 0, top); + System.arraycopy(stack, top + skip, cleared, top, current); + throwable.setStackTrace(cleared); + + return throwable; + } catch (RuntimeException ignored) { + // This should not happen unless someone instrumented or manipulated exception stack + // traces. + return throwable; + } + } + + /** + * Get the method of {@code instance} specified by {@code methodWithTypeAndSignature}. + * + * @param instance instance the method belongs to + * @param methodWithTypeAndSignature the description of the method + * + * @return method {@code methodWithTypeAndSignature} refer to + */ + @SuppressWarnings("unused") + public Method getOrigin(Object instance, String methodWithTypeAndSignature) throws Throwable { + if (!isMocked(instance)) { + return null; + } + + Matcher methodComponents = methodPattern.matcher(methodWithTypeAndSignature); + boolean wasFound = methodComponents.find(); + if (!wasFound) { + throw new IllegalArgumentException(); + } + String argTypeNames[] = methodComponents.group(3).split(","); + + ArrayList<Class<?>> argTypes = new ArrayList<>(argTypeNames.length); + for (String argTypeName : argTypeNames) { + if (!argTypeName.equals("")) { + switch (argTypeName) { + case "byte": + argTypes.add(Byte.TYPE); + break; + case "short": + argTypes.add(Short.TYPE); + break; + case "int": + argTypes.add(Integer.TYPE); + break; + case "long": + argTypes.add(Long.TYPE); + break; + case "char": + argTypes.add(Character.TYPE); + break; + case "float": + argTypes.add(Float.TYPE); + break; + case "double": + argTypes.add(Double.TYPE); + break; + case "boolean": + argTypes.add(Boolean.TYPE); + break; + case "byte[]": + argTypes.add(byte[].class); + break; + case "short[]": + argTypes.add(short[].class); + break; + case "int[]": + argTypes.add(int[].class); + break; + case "long[]": + argTypes.add(long[].class); + break; + case "char[]": + argTypes.add(char[].class); + break; + case "float[]": + argTypes.add(float[].class); + break; + case "double[]": + argTypes.add(double[].class); + break; + case "boolean[]": + argTypes.add(boolean[].class); + break; + default: + if (argTypeName.endsWith("[]")) { + argTypes.add(Class.forName("[L" + argTypeName.substring(0, + argTypeName.length() - 2) + ";")); + } else { + argTypes.add(Class.forName(argTypeName)); + } + break; + } + } + } + + Method origin = Class.forName(methodComponents.group(1)).getDeclaredMethod( + methodComponents.group(2), argTypes.toArray(new Class<?>[]{})); + + if (isOverridden(instance, origin)) { + return null; + } else { + return origin; + } + } + + /** + * Handle a method entry hook. + * + * @param instance instance that is mocked + * @param origin method that contains the hook + * @param arguments arguments to the method + * + * @return A callable that can be called to get the mocked result or null if the method is not + * mocked. + */ + @SuppressWarnings("unused") + public Callable<?> handle(Object instance, Method origin, Object[] arguments) throws Throwable { + InvocationHandlerAdapter interceptor = interceptors.get(instance); + if (interceptor == null) { + return null; + } + + return new ReturnValueWrapper(interceptor.interceptEntryHook(instance, origin, arguments, + new SuperMethodCall(selfCallInfo, origin, instance, arguments))); + } + + /** + * Checks if an {@code instance} is a mock. + * + * @param instance instance that might be a mock + * + * @return {@code true} iff the instance is a mock + */ + public boolean isMock(Object instance) { + return interceptors.containsKey(instance); + } + + /** + * Check if this method call should be mocked. Usually the same as {@link #isMock(Object)} but + * takes into account the state of {@link #selfCallInfo} that allows to temporary disable + * mocking for a single method call. + * + * @param instance instance that might be mocked + * + * @return {@code true} iff the a method call should be mocked + * + * @see SelfCallInfo + */ + public boolean isMocked(Object instance) { + return selfCallInfo.shouldMockMethod(instance) && isMock(instance); + } + + /** + * Check if a method is overridden. + * + * @param instance mocked instance + * @param origin method that might be overridden + * + * @return {@code true} iff the method is overridden + */ + public boolean isOverridden(Object instance, Method origin) { + Class<?> currentType = instance.getClass(); + + do { + try { + return !origin.equals(currentType.getDeclaredMethod(origin.getName(), + origin.getParameterTypes())); + } catch (NoSuchMethodException ignored) { + currentType = currentType.getSuperclass(); + } + } while (currentType != null); + + return true; + } + + /** + * Used to call the read (non mocked) method. + */ + private static class SuperMethodCall implements InterceptedInvocation.SuperMethod { + private final SelfCallInfo selfCallInfo; + private final Method origin; + private final Object instance; + private final Object[] arguments; + + private SuperMethodCall(SelfCallInfo selfCallInfo, Method origin, Object instance, + Object[] arguments) { + this.selfCallInfo = selfCallInfo; + this.origin = origin; + this.instance = instance; + this.arguments = arguments; + } + + @Override + public boolean isInvokable() { + return true; + } + + /** + * Call the read (non mocked) method. + * + * @return Result of read method + * @throws Throwable thrown by the read method + */ + @Override + public Object invoke() throws Throwable { + if (!Modifier.isPublic(origin.getDeclaringClass().getModifiers() + & origin.getModifiers())) { + origin.setAccessible(true); + } + + // By setting instance in the the selfCallInfo, once single method call on this instance + // and thread will call the read method as isMocked will return false. + selfCallInfo.set(instance); + return tryInvoke(origin, instance, arguments); + } + + } + + /** + * Stores a return value of {@link #handle(Object, Method, Object[])} and returns in on + * {@link #call()}. + */ + private static class ReturnValueWrapper implements Callable<Object> { + private final Object returned; + + private ReturnValueWrapper(Object returned) { + this.returned = returned; + } + + @Override + public Object call() { + return returned; + } + } + + /** + * Used to call the original method. If a instance is {@link #set(Object)} + * {@link #shouldMockMethod(Object)} returns false for this instance once. + * + * <p>This is {@link ThreadLocal}, so a thread can {@link #set(Object)} and instance and then + * call {@link #shouldMockMethod(Object)} without interference. + * + * @see SuperMethodCall#invoke() + * @see #isMocked(Object) + */ + private static class SelfCallInfo extends ThreadLocal<Object> { + boolean shouldMockMethod(Object value) { + Object current = get(); + + if (current == value) { + set(null); + return false; + } else { + return true; + } + } + } +} diff --git a/dexmaker-mockito-inline/src/main/java/com/android/dx/mockito/inline/UnmodifiableClassException.java b/dexmaker-mockito-inline/src/main/java/com/android/dx/mockito/inline/UnmodifiableClassException.java new file mode 100644 index 0000000..96d451f --- /dev/null +++ b/dexmaker-mockito-inline/src/main/java/com/android/dx/mockito/inline/UnmodifiableClassException.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2017 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.mockito.inline; + +/** + * Exception thrown by {@link JvmtiAgent#requestTransformClasses(Class[])} if any of the supplied + * classed cannot be modified. + */ +class UnmodifiableClassException extends Exception { + UnmodifiableClassException(RuntimeException e) { + super(e); + } +} diff --git a/dexmaker-mockito-inline/src/main/jni/dexmakerjvmtiagent/agent.cc b/dexmaker-mockito-inline/src/main/jni/dexmakerjvmtiagent/agent.cc new file mode 100644 index 0000000..e00ada8 --- /dev/null +++ b/dexmaker-mockito-inline/src/main/jni/dexmakerjvmtiagent/agent.cc @@ -0,0 +1,886 @@ +/* + * Copyright (C) 2017 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. + */ + +#include <cstdlib> +#include <sstream> +#include <cstring> +#include <cassert> +#include <cstdarg> +#include <algorithm> + +#include <jni.h> + +#include "jvmti.h" + +#include <dex_ir.h> +#include <code_ir.h> +#include <dex_ir_builder.h> +#include <dex_utf8.h> +#include <writer.h> +#include <reader.h> +#include <instrumentation.h> + +using namespace dex; +using namespace lir; + +namespace com_android_dx_mockito_inline { +static jvmtiEnv* localJvmtiEnv; + +static jobject sTransformer; + +// Converts a class name to a type descriptor +// (ex. "java.lang.String" to "Ljava/lang/String;") +static std::string +ClassNameToDescriptor(const char* class_name) { + std::stringstream ss; + ss << "L"; + for (auto p = class_name; *p != '\0'; ++p) { + ss << (*p == '.' ? '/' : *p); + } + ss << ";"; + return ss.str(); +} + +// Takes the full dex file for class 'classBeingRedefined' +// - isolates the dex code for the class out of the dex file +// - calls sTransformer.runTransformers on the isolated dex code +// - send the transformed code back to the runtime +static void +Transform(jvmtiEnv* jvmti_env, + JNIEnv* env, + jclass classBeingRedefined, + jobject loader, + const char* name, + jobject protectionDomain, + jint classDataLen, + const unsigned char* classData, + jint* newClassDataLen, + unsigned char** newClassData) { + if (sTransformer != NULL) { + // Isolate byte code of class class. This is needed as Android usually gives us more + // than the class we need. + Reader reader(classData, classDataLen); + + u4 index = reader.FindClassIndex(ClassNameToDescriptor(name).c_str()); + reader.CreateClassIr(index); + std::shared_ptr<ir::DexFile> ir = reader.GetIr(); + + struct Allocator : public Writer::Allocator { + virtual void* Allocate(size_t size) {return ::malloc(size);} + virtual void Free(void* ptr) {::free(ptr);} + }; + + Allocator allocator; + Writer writer(ir); + size_t isolatedClassLen = 0; + std::shared_ptr<jbyte> isolatedClass((jbyte*)writer.CreateImage(&allocator, + &isolatedClassLen)); + + // Create jbyteArray with isolated byte code of class + jbyteArray isolatedClassArr = env->NewByteArray(isolatedClassLen); + env->SetByteArrayRegion(isolatedClassArr, 0, isolatedClassLen, + isolatedClass.get()); + + jstring nameStr = env->NewStringUTF(name); + + // Call JvmtiAgent#runTransformers + jclass cls = env->GetObjectClass(sTransformer); + jmethodID runTransformers = env->GetMethodID(cls, "runTransformers", + "(Ljava/lang/ClassLoader;" + "Ljava/lang/String;" + "Ljava/lang/Class;" + "Ljava/security/ProtectionDomain;" + "[B)[B"); + + jbyteArray transformedArr = (jbyteArray) env->CallObjectMethod(sTransformer, + runTransformers, + loader, nameStr, + classBeingRedefined, + protectionDomain, + isolatedClassArr); + + // Set transformed byte code + if (!env->ExceptionOccurred() && transformedArr != NULL) { + *newClassDataLen = env->GetArrayLength(transformedArr); + + jbyte* transformed = env->GetByteArrayElements(transformedArr, 0); + + jvmti_env->Allocate(*newClassDataLen, newClassData); + std::memcpy(*newClassData, transformed, *newClassDataLen); + + env->ReleaseByteArrayElements(transformedArr, transformed, 0); + } + } +} + +// Add a label before instructionAfter +static void +addLabel(CodeIr& c, + lir::Instruction* instructionAfter, + Label* returnTrueLabel) { + c.instructions.InsertBefore(instructionAfter, returnTrueLabel); +} + +// Add a byte code before instructionAfter +static void +addInstr(CodeIr& c, + lir::Instruction* instructionAfter, + Opcode opcode, + const std::list<Operand*>& operands) { + auto instruction = c.Alloc<Bytecode>(); + + instruction->opcode = opcode; + + for (auto it = operands.begin(); it != operands.end(); it++) { + instruction->operands.push_back(*it); + } + + c.instructions.InsertBefore(instructionAfter, instruction); +} + +// Add a method call byte code before instructionAfter +static void +addCall(ir::Builder& b, + CodeIr& c, + lir::Instruction* instructionAfter, + Opcode opcode, + ir::Type* type, + const char* methodName, + ir::Type* returnType, + const std::vector<ir::Type*>& types, + const std::list<int>& regs) { + auto proto = b.GetProto(returnType, b.GetTypeList(types)); + auto method = b.GetMethodDecl(b.GetAsciiString(methodName), proto, type); + + VRegList* param_regs = c.Alloc<VRegList>(); + for (auto it = regs.begin(); it != regs.end(); it++) { + param_regs->registers.push_back(*it); + } + + addInstr(c, instructionAfter, opcode, {param_regs, c.Alloc<Method>(method, + method->orig_index)}); +} + +typedef struct { + ir::Type* boxedType; + ir::Type* scalarType; + std::string unboxMethod; +} BoxingInfo; + +// Get boxing / unboxing info for a type +static BoxingInfo +getBoxingInfo(ir::Builder &b, + char typeCode) { + BoxingInfo boxingInfo; + + if (typeCode != 'L' && typeCode != '[') { + std::stringstream tmp; + tmp << typeCode; + boxingInfo.scalarType = b.GetType(tmp.str().c_str()); + } + + switch (typeCode) { + case 'B': + boxingInfo.boxedType = b.GetType("Ljava/lang/Byte;"); + boxingInfo.unboxMethod = "byteValue"; + break; + case 'S': + boxingInfo.boxedType = b.GetType("Ljava/lang/Short;"); + boxingInfo.unboxMethod = "shortValue"; + break; + case 'I': + boxingInfo.boxedType = b.GetType("Ljava/lang/Integer;"); + boxingInfo.unboxMethod = "intValue"; + break; + case 'C': + boxingInfo.boxedType = b.GetType("Ljava/lang/Character;"); + boxingInfo.unboxMethod = "charValue"; + break; + case 'F': + boxingInfo.boxedType = b.GetType("Ljava/lang/Float;"); + boxingInfo.unboxMethod = "floatValue"; + break; + case 'Z': + boxingInfo.boxedType = b.GetType("Ljava/lang/Boolean;"); + boxingInfo.unboxMethod = "booleanValue"; + break; + case 'J': + boxingInfo.boxedType = b.GetType("Ljava/lang/Long;"); + boxingInfo.unboxMethod = "longValue"; + break; + case 'D': + boxingInfo.boxedType = b.GetType("Ljava/lang/Double;"); + boxingInfo.unboxMethod = "doubleValue"; + break; + default: + // real object + break; + } + + return boxingInfo; +} + +static size_t +getNumParams(ir::EncodedMethod *method) { + if (method->decl->prototype->param_types == NULL) { + return 0; + } + + return method->decl->prototype->param_types->types.size(); +} + +static bool +canBeTransformed(ir::EncodedMethod *method) { + std::string type = method->decl->parent->Decl(); + ir::String* methodName = method->decl->name; + + return !(((method->access_flags & (kAccAbstract | kAccPrivate | kAccBridge | kAccNative + | kAccStatic)) != 0) + || (Utf8Cmp(methodName->c_str(), "<init>") == 0) + || (Utf8Cmp(methodName->c_str(), "<clinit>") == 0) + || (Utf8Cmp(type.c_str(), "java.lang.Object") == 0 + && Utf8Cmp(methodName->c_str(), "finalize") == 0 + && getNumParams(method) == 0) + || (strncmp(type.c_str(), "java.", 5) == 0 + && (method->access_flags & (kAccPrivate | kAccPublic | kAccProtected)) == 0) + // getClass is used by MockMethodAdvice.isOverridden + || (Utf8Cmp(methodName->c_str(), "getClass") == 0)); +} + +static bool +isHashCode(ir::EncodedMethod *method) { + return Utf8Cmp(method->decl->name->c_str(), "hashCode") == 0 + && getNumParams(method) == 0; +} + +static bool +isEquals(ir::EncodedMethod *method) { + return Utf8Cmp(method->decl->name->c_str(), "equals") == 0 + && getNumParams(method) == 1 + && Utf8Cmp(method->decl->prototype->param_types->types[0]->Decl().c_str(), + "java.lang.Object") == 0; +} + +// Transforms the classes to add the mockito hooks +// - equals and hashcode are handled in a special way +extern "C" JNIEXPORT jbyteArray JNICALL +Java_com_android_dx_mockito_inline_ClassTransformer_nativeRedefine(JNIEnv* env, + jobject generator, + jstring idStr, + jbyteArray originalArr) { + unsigned char* original = (unsigned char*)env->GetByteArrayElements(originalArr, 0); + + Reader reader(original, env->GetArrayLength(originalArr)); + reader.CreateClassIr(0); + std::shared_ptr<ir::DexFile> dex_ir = reader.GetIr(); + ir::Builder b(dex_ir); + + ir::Type* booleanScalarT = b.GetType("Z"); + ir::Type* intScalarT = b.GetType("I"); + ir::Type* objectT = b.GetType("Ljava/lang/Object;"); + ir::Type* objectArrayT = b.GetType("[Ljava/lang/Object;"); + ir::Type* stringT = b.GetType("Ljava/lang/String;"); + ir::Type* methodT = b.GetType("Ljava/lang/reflect/Method;"); + ir::Type* systemT = b.GetType("Ljava/lang/System;"); + ir::Type* callableT = b.GetType("Ljava/util/concurrent/Callable;"); + ir::Type* dispatcherT = b.GetType("Lcom/android/dx/mockito/inline/MockMethodDispatcher;"); + + // Add id to dex file + const char* idNative = env->GetStringUTFChars(idStr, 0); + ir::String* id = b.GetAsciiString(idNative); + env->ReleaseStringUTFChars(idStr, idNative); + + for (auto& method : dex_ir->encoded_methods) { + if (!canBeTransformed(method.get())) { + continue; + } + + if (isEquals(method.get())) { + /* + equals_original(Object other) { + T t = foo(other); + return bar(t); + } + + equals_transformed(params) { + // MockMethodDispatcher dispatcher = MockMethodDispatcher.get(idStr, this); + const-string v0, "65463hg34t" + move-objectfrom16 v1, THIS + invoke-static {v0, v1}, MockMethodDispatcher.get(String, Object):MockMethodDispatcher + move-result-object v2 + + // if (dispatcher == null || ) { + // goto original_method; + // } + if-eqz v2, original_method + + // if (!dispatcher.isMock(this)) { + // goto original_method; + // } + invoke-virtual {v2, v1}, MockMethodDispatcher.isMock(Object):Method + move-result v2 + if-eqz v2, original_method + + // return self == other + move-objectfrom16 v0, ARG1 + if-eq v0, v1, return_true + + const v0, 0 + return v0 + + return true: + const v0, 1 + return v0 + + original_method: + // Move all method arguments down so that they match what the original code expects. + move-object16 v4, v5 # THIS + move-object16 v5, v6 # ARG1 + + T t = foo(other); + return bar(t); + } + */ + + CodeIr c(method.get(), dex_ir); + + // Make sure there are at least 5 local registers to use + int originalNumRegisters = method->code->registers - method->code->ins_count; + int numAdditionalRegs = std::max(0, 3 - originalNumRegisters); + int thisReg = numAdditionalRegs + method->code->registers + - method->code->ins_count; + + if (numAdditionalRegs > 0) { + c.ir_method->code->registers += numAdditionalRegs; + } + + lir::Instruction* fi = *(c.instructions.begin()); + + Label* originalMethodLabel = c.Alloc<Label>(0); + Label* returnTrueLabel = c.Alloc<Label>(0); + CodeLocation* originalMethod = c.Alloc<CodeLocation>(originalMethodLabel); + VReg* v0 = c.Alloc<VReg>(0); + VReg* v1 = c.Alloc<VReg>(1); + VReg* v2 = c.Alloc<VReg>(2); + VReg* thiz = c.Alloc<VReg>(thisReg); + + addInstr(c, fi, OP_CONST_STRING, {v0, c.Alloc<String>(id, id->orig_index)}); + addInstr(c, fi, OP_MOVE_OBJECT_FROM16, {v1, thiz}); + addCall(b, c, fi, OP_INVOKE_STATIC, dispatcherT, "get", dispatcherT, + {stringT, objectT}, {0, 1}); + addInstr(c, fi, OP_MOVE_RESULT_OBJECT, {v2}); + addInstr(c, fi, OP_IF_EQZ, {v2, originalMethod}); + addCall(b, c, fi, OP_INVOKE_VIRTUAL, dispatcherT, "isMock", booleanScalarT, {objectT}, + {2, 1}); + addInstr(c, fi, OP_MOVE_RESULT, {v2}); + addInstr(c, fi, OP_IF_EQZ, {v2, originalMethod}); + addInstr(c, fi, OP_MOVE_OBJECT_FROM16, {v0, c.Alloc<VReg>(thisReg + 1)}); + addInstr(c, fi, OP_IF_EQ, {v0, v1, c.Alloc<CodeLocation>(returnTrueLabel)}); + addInstr(c, fi, OP_CONST, {v0, c.Alloc<Const32>(0)}); + addInstr(c, fi, OP_RETURN, {v0}); + addLabel(c, fi, returnTrueLabel); + addInstr(c, fi, OP_CONST, {v0, c.Alloc<Const32>(1)}); + addInstr(c, fi, OP_RETURN, {v0}); + addLabel(c, fi, originalMethodLabel); + addInstr(c, fi, OP_MOVE_OBJECT_16, {c.Alloc<VReg>(thisReg - numAdditionalRegs), thiz}); + addInstr(c, fi, OP_MOVE_OBJECT_16, {c.Alloc<VReg>(thisReg - numAdditionalRegs + 1), + c.Alloc<VReg>(thisReg + 1)}); + + c.Assemble(); + } else if (isHashCode(method.get())) { + /* + hashCode_original(Object other) { + T t = foo(other); + return bar(t); + } + + hashCode_transformed(params) { + // MockMethodDispatcher dispatcher = MockMethodDispatcher.get(idStr, this); + const-string v0, "65463hg34t" + move-objectfrom16 v1, THIS + invoke-static {v0, v1}, MockMethodDispatcher.get(String, Object):MockMethodDispatcher + move-result-object v2 + + // if (dispatcher == null || ) { + // goto original_method; + // } + if-eqz v2, original_method + + // if (!dispatcher.isMock(this)) { + // goto original_method; + // } + invoke-interface {v2, v1}, MockMethodDispatcher.isMock(Object):Method + move-result v2 + if-eqz v2, original_method + + // return System.identityHashCode(this); + invoke-static {v1}, System.identityHashCode(Object):int + move-result v2 + return v2 + + original_method: + // Move all method arguments down so that they match what the original code expects. + move-object16 v4, v5 # THIS + + T t = foo(other); + return bar(t); + } + */ + + CodeIr c(method.get(), dex_ir); + + // Make sure there are at least 5 local registers to use + int originalNumRegisters = method->code->registers - method->code->ins_count; + int numAdditionalRegs = std::max(0, 3 - originalNumRegisters); + int thisReg = numAdditionalRegs + method->code->registers - method->code->ins_count; + + if (numAdditionalRegs > 0) { + c.ir_method->code->registers += numAdditionalRegs; + } + + lir::Instruction* fi = *(c.instructions.begin()); + + Label* originalMethodLabel = c.Alloc<Label>(0); + CodeLocation* originalMethod = c.Alloc<CodeLocation>(originalMethodLabel); + VReg* v0 = c.Alloc<VReg>(0); + VReg* v1 = c.Alloc<VReg>(1); + VReg* v2 = c.Alloc<VReg>(2); + VReg* thiz = c.Alloc<VReg>(thisReg); + + addInstr(c, fi, OP_CONST_STRING, {v0, c.Alloc<String>(id, id->orig_index)}); + addInstr(c, fi, OP_MOVE_OBJECT_FROM16, {v1, thiz}); + addCall(b, c, fi, OP_INVOKE_STATIC, dispatcherT, "get", dispatcherT, + {stringT, objectT}, {0, 1}); + addInstr(c, fi, OP_MOVE_RESULT_OBJECT, {v2}); + addInstr(c, fi, OP_IF_EQZ, {v2, originalMethod}); + addCall(b, c, fi, OP_INVOKE_VIRTUAL, dispatcherT, "isMock", booleanScalarT, {objectT}, + {2, 1}); + addInstr(c, fi, OP_MOVE_RESULT, {v2}); + addInstr(c, fi, OP_IF_EQZ, {v2, originalMethod}); + addCall(b, c, fi, OP_INVOKE_STATIC, systemT, "identityHashCode", intScalarT, {objectT}, + {1}); + addInstr(c, fi, OP_MOVE_RESULT, {v2}); + addInstr(c, fi, OP_RETURN, {v2}); + addLabel(c, fi, originalMethodLabel); + addInstr(c, fi, OP_MOVE_OBJECT_16, {c.Alloc<VReg>(thisReg - numAdditionalRegs), thiz}); + + c.Assemble(); + } else { + /* + long method_original(int param1, long param2, String param3) { + foo(); + return bar(); + } + + long method_transformed(int param1, long param2, String param3) { + // MockMethodDispatcher dispatcher = MockMethodDispatcher.get(idStr, this); + const-string v0, "65463hg34t" + move-objectfrom16 v1, THIS # this is necessary as invoke-static cannot deal + # with medium or high registers and THIS might not + # be low + invoke-static {v0, v1}, MockMethodDispatcher.get(String, Object):MockMethodDispatcher + move-result-object v0 + + // if (dispatcher == null) { + // goto original_method; + // } + if-eqz v0, original_method + + // Method origin = dispatcher.getOrigin(this, methodDesc); + const-string v1 "fully.qualified.ClassName#original_method(int, long, String)" + move-objectfrom16 v2, THIS # this is necessary as invoke-static cannot deal + # with medium or high registers and THIS might not + # be low + invoke-virtual {v0, v2, v1}, MockMethodDispatcher.getOrigin(Object, String):Method + move-result-object v1 + + // if (origin == null) { + // goto original_method; + // } + if-eqz v1, original_method + + // Create an array with Objects of all parameters. + + // Object[] arguments = new Object[3] + const v3, 3 + new-array v2, v3, Object[] + + // Integer param1Integer = Integer.valueOf(param1) + move-from16 v3, ARG1 # this is necessary as invoke-static cannot deal with high + # registers and ARG1 might be high + invoke-static {v3}, Integer.valueOf(int):Integer + move-result-object v3 + + // arguments[0] = param1Integer + const v4, 0 + aput-object v3, v2, v4 + + // Long param2Long = Long.valueOf(param2) + move-widefrom16 v3:v4, ARG2.1:ARG2.2 # this is necessary as invoke-static cannot + # deal with high registers and ARG2 might be + # high + invoke-static {v3, v4}, Long.valueOf(long):Long + move-result-object v3 + + // arguments[1] = param2Long + const v4, 1 + aput-object v3, v2, v4 + + // arguments[2] = param3 + const v4, 2 + move-objectfrom16 v3, ARG3 # this is necessary as aput-object cannot deal with + # high registers and ARG3 might be high + aput-object v3, v2, v4 + + // Callable<?> mocked = dispatcher.handle(this, origin, arguments); + move-objectfrom16 v3, THIS # this is necessary as invoke-virtual cannot deal + # with medium or high registers and THIS might not + # be low + invoke-virtual {v0,v3,v1,v2}, MockMethodDispatcher.handle(Object, Method, + Object[]):Callable + move-result-object v0 + + // if (mocked != null) { + if-eqz v0, original_method + + // Object ret = mocked.call(); + invoke-interface {v0}, Callable.call():Object + move-result-object v0 + + // Long retLong = (Long)ret + check-cast v0, Long + + // long retlong = retLong.longValue(); + invoke-virtual {v0}, Long.longValue():long + move-result-wide v0:v1 + + // return retlong; + return-wide v0:v1 + + // } + + original_method: + // Move all method arguments down so that they match what the original code expects. + // Let's assume three arguments, one int, one long, one String and the and used to + // use 4 registers + move-object16 v4, v5 # THIS + move16 v5, v6 # ARG1 + move-wide16 v6:v7, v7:v8 # ARG2 (overlapping moves are allowed) + move-object16 v8, v9 # ARG3 + + // foo(); + // return bar(); + unmodified original byte code + } + */ + + CodeIr c(method.get(), dex_ir); + + // Make sure there are at least 5 local registers to use + int originalNumRegisters = method->code->registers - method->code->ins_count; + int numAdditionalRegs = std::max(0, 5 - originalNumRegisters); + int thisReg = originalNumRegisters + numAdditionalRegs; + + if (numAdditionalRegs > 0) { + c.ir_method->code->registers += numAdditionalRegs; + } + + lir::Instruction* fi = *(c.instructions.begin()); + + // Add methodDesc to dex file + std::stringstream ss; + ss << method->decl->parent->Decl() << "#" << method->decl->name->c_str() << "(" ; + bool first = true; + if (method->decl->prototype->param_types != NULL) { + for (const auto& type : method->decl->prototype->param_types->types) { + if (first) { + first = false; + } else { + ss << ","; + } + + ss << type->Decl().c_str(); + } + } + ss << ")"; + std::string methodDescStr = ss.str(); + ir::String* methodDesc = b.GetAsciiString(methodDescStr.c_str()); + + size_t numParams = getNumParams(method.get()); + + Label* originalMethodLabel = c.Alloc<Label>(0); + CodeLocation* originalMethod = c.Alloc<CodeLocation>(originalMethodLabel); + VReg* v0 = c.Alloc<VReg>(0); + VReg* v1 = c.Alloc<VReg>(1); + VReg* v2 = c.Alloc<VReg>(2); + VReg* v3 = c.Alloc<VReg>(3); + VReg* v4 = c.Alloc<VReg>(4); + VReg* thiz = c.Alloc<VReg>(thisReg); + + addInstr(c, fi, OP_CONST_STRING, {v0, c.Alloc<String>(id, id->orig_index)}); + addInstr(c, fi, OP_MOVE_OBJECT_FROM16, {v1, thiz}); + addCall(b, c, fi, OP_INVOKE_STATIC, dispatcherT, "get", dispatcherT, {stringT, objectT}, + {0, 1}); + addInstr(c, fi, OP_MOVE_RESULT_OBJECT, {v0}); + addInstr(c, fi, OP_IF_EQZ, {v0, originalMethod}); + addInstr(c, fi, OP_CONST_STRING, + {v1, c.Alloc<String>(methodDesc, methodDesc->orig_index)}); + addInstr(c, fi, OP_MOVE_OBJECT_FROM16, {v2, thiz}); + addCall(b, c, fi, OP_INVOKE_VIRTUAL, dispatcherT, "getOrigin", methodT, + {objectT, stringT}, {0, 2, 1}); + addInstr(c, fi, OP_MOVE_RESULT_OBJECT, {v1}); + addInstr(c, fi, OP_IF_EQZ, {v1, originalMethod}); + addInstr(c, fi, OP_CONST, {v3, c.Alloc<Const32>(numParams)}); + addInstr(c, fi, OP_NEW_ARRAY, {v2, v3, c.Alloc<Type>(objectArrayT, + objectArrayT->orig_index)}); + + if (numParams > 0) { + int argReg = thisReg + 1; + + for (int argNum = 0; argNum < numParams; argNum++) { + const auto& type = method->decl->prototype->param_types->types[argNum]; + BoxingInfo boxingInfo = getBoxingInfo(b, type->descriptor->c_str()[0]); + + switch (type->GetCategory()) { + case ir::Type::Category::Scalar: + addInstr(c, fi, OP_MOVE_FROM16, {v3, c.Alloc<VReg>(argReg)}); + addCall(b, c, fi, OP_INVOKE_STATIC, boxingInfo.boxedType, "valueOf", + boxingInfo.boxedType, {type}, {3}); + addInstr(c, fi, OP_MOVE_RESULT_OBJECT, {v3}); + + argReg++; + break; + case ir::Type::Category::WideScalar: { + VRegPair* v3v4 = c.Alloc<VRegPair>(3); + VRegPair* argRegPair = c.Alloc<VRegPair>(argReg); + + addInstr(c, fi, OP_MOVE_WIDE_FROM16, {v3v4, argRegPair}); + addCall(b, c, fi, OP_INVOKE_STATIC, boxingInfo.boxedType, "valueOf", + boxingInfo.boxedType, {type}, {3, 4}); + addInstr(c, fi, OP_MOVE_RESULT_OBJECT, {v3}); + + argReg += 2; + break; + } + case ir::Type::Category::Reference: + addInstr(c, fi, OP_MOVE_OBJECT_FROM16, {v3, c.Alloc<VReg>(argReg)}); + + argReg++; + break; + case ir::Type::Category::Void: + assert(false); + } + + addInstr(c, fi, OP_CONST, {v4, c.Alloc<Const32>(argNum)}); + addInstr(c, fi, OP_APUT_OBJECT, {v3, v2, v4}); + } + } + + addInstr(c, fi, OP_MOVE_OBJECT_FROM16, {v3, thiz}); + addCall(b, c, fi, OP_INVOKE_VIRTUAL, dispatcherT, "handle", callableT, + {objectT, methodT, objectArrayT}, {0, 3, 1, 2}); + addInstr(c, fi, OP_MOVE_RESULT_OBJECT, {v0}); + addInstr(c, fi, OP_IF_EQZ, {v0, originalMethod}); + addCall(b, c, fi, OP_INVOKE_INTERFACE, callableT, "call", objectT, {}, {0}); + addInstr(c, fi, OP_MOVE_RESULT_OBJECT, {v0}); + + ir::Type *returnType = method->decl->prototype->return_type; + BoxingInfo boxingInfo = getBoxingInfo(b, returnType->descriptor->c_str()[0]); + + switch (returnType->GetCategory()) { + case ir::Type::Category::Scalar: + addInstr(c, fi, OP_CHECK_CAST, {v0, + c.Alloc<Type>(boxingInfo.boxedType, boxingInfo.boxedType->orig_index)}); + addCall(b, c, fi, OP_INVOKE_VIRTUAL, boxingInfo.boxedType, + boxingInfo.unboxMethod.c_str(), returnType, {}, {0}); + addInstr(c, fi, OP_MOVE_RESULT, {v0}); + addInstr(c, fi, OP_RETURN, {v0}); + break; + case ir::Type::Category::WideScalar: { + VRegPair* v0v1 = c.Alloc<VRegPair>(0); + + addInstr(c, fi, OP_CHECK_CAST, {v0, + c.Alloc<Type>(boxingInfo.boxedType, boxingInfo.boxedType->orig_index)}); + addCall(b, c, fi, OP_INVOKE_VIRTUAL, boxingInfo.boxedType, + boxingInfo.unboxMethod.c_str(), returnType, {}, {0}); + addInstr(c, fi, OP_MOVE_RESULT_WIDE, {v0v1}); + addInstr(c, fi, OP_RETURN_WIDE, {v0v1}); + break; + } + case ir::Type::Category::Reference: + addInstr(c, fi, OP_CHECK_CAST, {v0, c.Alloc<Type>(returnType, + returnType->orig_index)}); + addInstr(c, fi, OP_RETURN_OBJECT, {v0}); + break; + case ir::Type::Category::Void: + addInstr(c, fi, OP_RETURN_VOID, {}); + break; + } + + addLabel(c, fi, originalMethodLabel); + addInstr(c, fi, OP_MOVE_OBJECT_16, {c.Alloc<VReg>(thisReg - numAdditionalRegs), thiz}); + + if (numParams > 0) { + int argReg = thisReg + 1; + + for (int argNum = 0; argNum < numParams; argNum++) { + const auto& type = method->decl->prototype->param_types->types[argNum]; + int origReg = argReg - numAdditionalRegs; + switch (type->GetCategory()) { + case ir::Type::Category::Scalar: + addInstr(c, fi, OP_MOVE_16, {c.Alloc<VReg>(origReg), + c.Alloc<VReg>(argReg)}); + argReg++; + break; + case ir::Type::Category::WideScalar: + addInstr(c, fi, OP_MOVE_WIDE_16,{c.Alloc<VRegPair>(origReg), + c.Alloc<VRegPair>(argReg)}); + argReg +=2; + break; + case ir::Type::Category::Reference: + addInstr(c, fi, OP_MOVE_OBJECT_16, {c.Alloc<VReg>(origReg), + c.Alloc<VReg>(argReg)}); + argReg++; + break; + } + } + } + + c.Assemble(); + } + } + + struct Allocator : public Writer::Allocator { + virtual void* Allocate(size_t size) {return ::malloc(size);} + virtual void Free(void* ptr) {::free(ptr);} + }; + + Allocator allocator; + Writer writer(dex_ir); + size_t transformedLen = 0; + std::shared_ptr<jbyte> transformed((jbyte*)writer.CreateImage(&allocator, &transformedLen)); + + jbyteArray transformedArr = env->NewByteArray(transformedLen); + env->SetByteArrayRegion(transformedArr, 0, transformedLen, transformed.get()); + + return transformedArr; +} + +// Initializes the agent +extern "C" jint Agent_OnAttach(JavaVM* vm, + char* options, + void* reserved) { + jint jvmError = vm->GetEnv(reinterpret_cast<void**>(&localJvmtiEnv), JVMTI_VERSION_1_2); + if (jvmError != JNI_OK) { + return jvmError; + } + + jvmtiCapabilities caps; + memset(&caps, 0, sizeof(caps)); + caps.can_retransform_classes = 1; + + jvmtiError error = localJvmtiEnv->AddCapabilities(&caps); + if (error != JVMTI_ERROR_NONE) { + return error; + } + + jvmtiEventCallbacks cb; + memset(&cb, 0, sizeof(cb)); + cb.ClassFileLoadHook = Transform; + + error = localJvmtiEnv->SetEventCallbacks(&cb, sizeof(cb)); + if (error != JVMTI_ERROR_NONE) { + return error; + } + + error = localJvmtiEnv->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_CLASS_FILE_LOAD_HOOK, + NULL); + if (error != JVMTI_ERROR_NONE) { + return error; + } + + return JVMTI_ERROR_NONE; +} + +// Throw runtime exception +static void throwRuntimeExpection(JNIEnv* env, const char* fmt, ...) { + char msgBuf[512]; + + va_list args; + va_start (args, fmt); + vsnprintf(msgBuf, sizeof(msgBuf), fmt, args); + va_end (args); + + jclass exceptionClass = env->FindClass("java/lang/RuntimeException"); + env->ThrowNew(exceptionClass, msgBuf); +} + +// Register transformer hook +extern "C" JNIEXPORT void JNICALL +Java_com_android_dx_mockito_inline_JvmtiAgent_nativeRegisterTransformerHook(JNIEnv* env, + jobject thiz) { + sTransformer = env->NewGlobalRef(thiz); +} + +// Unregister transformer hook +extern "C" JNIEXPORT void JNICALL +Java_com_android_dx_mockito_inline_JvmtiAgent_nativeUnregisterTransformerHook(JNIEnv* env, + jobject thiz) { + env->DeleteGlobalRef(sTransformer); + sTransformer = NULL; +} + +// Triggers retransformation of classes via this file's Transform method +extern "C" JNIEXPORT void JNICALL +Java_com_android_dx_mockito_inline_JvmtiAgent_nativeRetransformClasses(JNIEnv* env, + jobject thiz, + jobjectArray classes) { + jsize numTransformedClasses = env->GetArrayLength(classes); + jclass *transformedClasses = (jclass*) malloc(numTransformedClasses * sizeof(jclass)); + for (int i = 0; i < numTransformedClasses; i++) { + transformedClasses[i] = (jclass) env->NewGlobalRef(env->GetObjectArrayElement(classes, i)); + } + + jvmtiError error = localJvmtiEnv->RetransformClasses(numTransformedClasses, + transformedClasses); + + for (int i = 0; i < numTransformedClasses; i++) { + env->DeleteGlobalRef(transformedClasses[i]); + } + free(transformedClasses); + + if (error != JVMTI_ERROR_NONE) { + throwRuntimeExpection(env, "Could not retransform classes: %d", error); + } +} + +// Adds a jar file to the bootstrap class loader +extern "C" JNIEXPORT void JNICALL +Java_com_android_dx_mockito_inline_JvmtiAgent_nativeAppendToBootstrapClassLoaderSearch(JNIEnv* env, + jclass klass, + jstring jarFile) { + const char *jarFileNative = env->GetStringUTFChars(jarFile, 0); + jvmtiError error = localJvmtiEnv->AddToBootstrapClassLoaderSearch(jarFileNative); + + if (error != JVMTI_ERROR_NONE) { + throwRuntimeExpection(env, "Could not add %s to bootstrap class path: %d", jarFileNative, + error); + } + + env->ReleaseStringUTFChars(jarFile, jarFileNative); +} +} // namespace com_android_dx_mockito_inline + diff --git a/dexmaker-mockito-inline/src/main/resources/README.txt b/dexmaker-mockito-inline/src/main/resources/README.txt new file mode 100644 index 0000000..b94264b --- /dev/null +++ b/dexmaker-mockito-inline/src/main/resources/README.txt @@ -0,0 +1,2 @@ +dispatcher.jar is the classes.dex of the apk created by dexmaker-mockito-inline-dispatcher +repackaged into a jar. We should automate this.
\ No newline at end of file diff --git a/dexmaker-mockito-inline/src/main/resources/dispatcher.jar b/dexmaker-mockito-inline/src/main/resources/dispatcher.jar Binary files differnew file mode 100644 index 0000000..6a5cf5f --- /dev/null +++ b/dexmaker-mockito-inline/src/main/resources/dispatcher.jar diff --git a/dexmaker-mockito-inline/src/main/resources/mockito-extensions/org.mockito.plugins.MockMaker b/dexmaker-mockito-inline/src/main/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 0000000..4f2b284 --- /dev/null +++ b/dexmaker-mockito-inline/src/main/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +com.android.dx.mockito.inline.InlineDexmakerMockMaker
\ No newline at end of file diff --git a/dexmaker-mockito-inline/src/main/resources/mockito-extensions/org.mockito.plugins.StackTraceCleanerProvider b/dexmaker-mockito-inline/src/main/resources/mockito-extensions/org.mockito.plugins.StackTraceCleanerProvider new file mode 100644 index 0000000..1767c97 --- /dev/null +++ b/dexmaker-mockito-inline/src/main/resources/mockito-extensions/org.mockito.plugins.StackTraceCleanerProvider @@ -0,0 +1 @@ +com.android.dx.mockito.inline.DexmakerStackTraceCleaner
\ No newline at end of file diff --git a/dexmaker-mockito-tests/AndroidManifest.xml b/dexmaker-mockito-tests/AndroidManifest.xml new file mode 100644 index 0000000..45201f9 --- /dev/null +++ b/dexmaker-mockito-tests/AndroidManifest.xml @@ -0,0 +1,3 @@ +<manifest package="com.android.dexmaker.mockito.tests"> + <application /> +</manifest> diff --git a/dexmaker-mockito-tests/build.gradle b/dexmaker-mockito-tests/build.gradle new file mode 100644 index 0000000..f0befe0 --- /dev/null +++ b/dexmaker-mockito-tests/build.gradle @@ -0,0 +1,31 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 25 + buildToolsVersion "25.0.0" + + lintOptions { + abortOnError false + } + + defaultConfig { + applicationId "com.android.dexmaker.mockito.tests" + minSdkVersion 25 + targetSdkVersion 25 + versionName VERSION_NAME + + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } +} + +repositories { + jcenter() +} + +dependencies { + compile project(':dexmaker') + compile project(':dexmaker-mockito') + + androidTestCompile 'com.android.support.test:runner:0.5' + androidTestCompile 'junit:junit:4.12' +} diff --git a/dexmaker-mockito-tests/src/androidTest/java/com/android/dx/mockito/tests/MockTests.java b/dexmaker-mockito-tests/src/androidTest/java/com/android/dx/mockito/tests/MockTests.java new file mode 100644 index 0000000..1102b71 --- /dev/null +++ b/dexmaker-mockito-tests/src/androidTest/java/com/android/dx/mockito/tests/MockTests.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2017 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.mockito.tests; + +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.exceptions.base.MockitoException; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(AndroidJUnit4.class) +public class MockTests { + public static class TestClass { + public String returnA() { + return "A"; + } + } + + public interface TestInterface { + String returnA(); + } + + @Test + public void mockClass() throws Exception { + TestClass t = mock(TestClass.class); + + assertNull(t.returnA()); + + when(t.returnA()).thenReturn("B"); + assertEquals("B", t.returnA()); + } + + @Test + public void mockInterface() throws Exception { + TestInterface t = mock(TestInterface.class); + + assertNull(t.returnA()); + + when(t.returnA()).thenReturn("B"); + assertEquals("B", t.returnA()); + } + + @Test + public void spyClass() throws Exception { + TestClass originalT = new TestClass(); + TestClass t = spy(originalT); + + assertEquals("A", t.returnA()); + + when(t.returnA()).thenReturn("B"); + assertEquals("B", t.returnA()); + + // Wrapped object is not affected by mocking + assertEquals("A", originalT.returnA()); + } + + @Test + public void spyNewClass() throws Exception { + TestClass t = spy(TestClass.class); + + assertEquals("A", t.returnA()); + + when(t.returnA()).thenReturn("B"); + assertEquals("B", t.returnA()); + } + + @Test + public void cleanStackTraceProxy() { + TestClass t = mock(TestClass.class); + + try { + verify(t).returnA(); + } catch (Throwable verifyLocation) { + try { + throw new Exception(); + } catch (Exception here) { + assertEquals(here.getStackTrace()[0].getMethodName(), verifyLocation + .getStackTrace()[0].getMethodName()); + } + } + } + + @Test + public void cleanStackTraceInterface() { + TestInterface t = mock(TestInterface.class); + + try { + verify(t).returnA(); + } catch (Throwable verifyLocation) { + try { + throw new Exception(); + } catch (Exception here) { + assertEquals(here.getStackTrace()[0].getMethodName(), verifyLocation + .getStackTrace()[0].getMethodName()); + } + } + } +} diff --git a/dexmaker-mockito/build.gradle b/dexmaker-mockito/build.gradle index 36fa92f..e1f5133 100644 --- a/dexmaker-mockito/build.gradle +++ b/dexmaker-mockito/build.gradle @@ -14,5 +14,5 @@ repositories { dependencies { compile project(":dexmaker") - compile 'org.mockito:mockito-core:2.2.29' + compile 'org.mockito:mockito-core:2.12.0' } diff --git a/dexmaker-mockito/src/main/java/com/android/dx/mockito/DexmakerMockMaker.java b/dexmaker-mockito/src/main/java/com/android/dx/mockito/DexmakerMockMaker.java index 4015059..f934fa6 100644 --- a/dexmaker-mockito/src/main/java/com/android/dx/mockito/DexmakerMockMaker.java +++ b/dexmaker-mockito/src/main/java/com/android/dx/mockito/DexmakerMockMaker.java @@ -109,12 +109,15 @@ public final class DexmakerMockMaker implements MockMaker, StackTraceCleanerProv return new StackTraceCleaner() { @Override public boolean isIn(StackTraceElement candidate) { + String className = candidate.getClassName(); + return defaultCleaner.isIn(candidate) - && !candidate.getClassName().endsWith("_Proxy") // dexmaker class proxies - && !candidate.getClassName().startsWith("$Proxy") // dalvik interface proxies - && !candidate.getClassName().startsWith("java.lang.reflect.Proxy") - && !candidate.getClassName().startsWith("com.google.dexmaker.mockito.") - && !candidate.getClassName().startsWith("com.android.dx.mockito."); + && !className.endsWith("_Proxy") // dexmaker class proxies + && !className.startsWith("$Proxy") // dalvik interface proxies + && !className.startsWith("java.lang.reflect.Proxy") + && !(className.startsWith("com.android.dx.mockito.") + // Do not clean unit tests + && !className.startsWith("com.android.dx.mockito.tests")); } }; } diff --git a/dexmaker-mockito/src/main/java/com/android/dx/mockito/InterceptedInvocation.java b/dexmaker-mockito/src/main/java/com/android/dx/mockito/InterceptedInvocation.java new file mode 100644 index 0000000..b0b42e9 --- /dev/null +++ b/dexmaker-mockito/src/main/java/com/android/dx/mockito/InterceptedInvocation.java @@ -0,0 +1,189 @@ +/* + * Copyright (c) 2016 Mockito contributors + * This program is made available under the terms of the MIT License. + */ + +package com.android.dx.mockito; + +import org.mockito.internal.debugging.LocationImpl; +import org.mockito.internal.exceptions.VerificationAwareInvocation; +import org.mockito.internal.invocation.ArgumentsProcessor; +import org.mockito.internal.invocation.MockitoMethod; +import org.mockito.internal.reporting.PrintSettings; +import org.mockito.invocation.Invocation; +import org.mockito.invocation.Location; +import org.mockito.invocation.StubInfo; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.Arrays; + +import static org.mockito.internal.exceptions.Reporter.cannotCallAbstractRealMethod; + +/** + * {@link Invocation} used when intercepting methods from an method entry hook. + */ +class InterceptedInvocation implements Invocation, VerificationAwareInvocation { + /** The mocked instance */ + private final Object mock; + + /** The method invoked */ + private final MockitoMethod method; + + /** expanded arguments to the method */ + private final Object[] arguments; + + /** raw arguments to the method */ + private final Object[] rawArguments; + + /** The super method */ + private final SuperMethod superMethod; + + /** sequence number of the invocation (different for each invocation) */ + private final int sequenceNumber; + + /** the location of the invocation (i.e. the stack trace) */ + private final Location location; + + /** Was this invocation {@link #markVerified() marked as verified} */ + private boolean verified; + + /** Should this be {@link #ignoreForVerification()} ignored for verification?} */ + private boolean isIgnoredForVerification; + + /** The stubinfo is this was {@link #markStubbed(StubInfo) markes as stubbed}*/ + private StubInfo stubInfo; + + /** + * Create a new invocation. + * + * @param mock mocked instance + * @param method method invoked + * @param arguments arguments to the method + * @param superMethod super method + * @param sequenceNumber sequence number of the invocation + */ + InterceptedInvocation(Object mock, MockitoMethod method, Object[] arguments, + SuperMethod superMethod, int sequenceNumber) { + this.mock = mock; + this.method = method; + this.arguments = ArgumentsProcessor.expandArgs(method, arguments); + this.rawArguments = arguments; + this.superMethod = superMethod; + this.sequenceNumber = sequenceNumber; + location = new LocationImpl(); + } + + @Override + public boolean isVerified() { + return verified || isIgnoredForVerification; + } + + @Override + public int getSequenceNumber() { + return sequenceNumber; + } + + @Override + public Location getLocation() { + return location; + } + + @Override + public Object[] getRawArguments() { + return rawArguments; + } + + @Override + public Class<?> getRawReturnType() { + return method.getReturnType(); + } + + @Override + public void markVerified() { + verified = true; + } + + @Override + public StubInfo stubInfo() { + return stubInfo; + } + + @Override + public void markStubbed(StubInfo stubInfo) { + this.stubInfo = stubInfo; + } + + @Override + public boolean isIgnoredForVerification() { + return isIgnoredForVerification; + } + + @Override + public void ignoreForVerification() { + isIgnoredForVerification = true; + } + + @Override + public Object getMock() { + return mock; + } + + @Override + public Method getMethod() { + return method.getJavaMethod(); + } + + @Override + public Object[] getArguments() { + return arguments; + } + + @Override + @SuppressWarnings("unchecked") + public <T> T getArgument(int index) { + return (T) arguments[index]; + } + + @Override + public Object callRealMethod() throws Throwable { + if (!superMethod.isInvokable()) { + throw cannotCallAbstractRealMethod(); + } + return superMethod.invoke(); + } + + @Override + public int hashCode() { + // TODO SF we need to provide hash code implementation so that there are no unexpected, + // slight perf issues + return 1; + } + + @Override + public boolean equals(Object o) { + if (o == null || !o.getClass().equals(this.getClass())) { + return false; + } + InterceptedInvocation other = (InterceptedInvocation) o; + return this.mock.equals(other.mock) + && this.method.equals(other.method) + && this.equalArguments(other.arguments); + } + + private boolean equalArguments(Object[] arguments) { + return Arrays.equals(arguments, this.arguments); + } + + @Override + public String toString() { + return new PrintSettings().print(ArgumentsProcessor.argumentsToMatchers(getArguments()), + this); + } + + interface SuperMethod extends Serializable { + boolean isInvokable(); + + Object invoke() throws Throwable; + } +} diff --git a/dexmaker-mockito/src/main/java/com/android/dx/mockito/InvocationHandlerAdapter.java b/dexmaker-mockito/src/main/java/com/android/dx/mockito/InvocationHandlerAdapter.java index e2a5957..6a6dc72 100644 --- a/dexmaker-mockito/src/main/java/com/android/dx/mockito/InvocationHandlerAdapter.java +++ b/dexmaker-mockito/src/main/java/com/android/dx/mockito/InvocationHandlerAdapter.java @@ -17,17 +17,22 @@ package com.android.dx.mockito; import com.android.dx.stock.ProxyBuilder; + +import org.mockito.internal.creation.DelegatingMethod; import org.mockito.internal.debugging.LocationImpl; -import org.mockito.internal.invocation.InvocationImpl; -import org.mockito.internal.invocation.MockitoMethod; -import org.mockito.internal.invocation.realmethod.RealMethod; +import org.mockito.internal.invocation.ArgumentsProcessor; import org.mockito.internal.progress.SequenceNumber; +import org.mockito.invocation.Invocation; +import org.mockito.invocation.Location; import org.mockito.invocation.MockHandler; +import org.mockito.invocation.StubInfo; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import static org.mockito.internal.exceptions.Reporter.cannotCallAbstractRealMethod; + /** * Handles proxy method invocations to dexmaker's InvocationHandler by calling * a MockitoInvocationHandler. @@ -49,9 +54,8 @@ final class InvocationHandlerAdapter implements InvocationHandler { return System.identityHashCode(proxy); } - ProxiedMethod proxiedMethod = new ProxiedMethod(method); - return handler.handle(new InvocationImpl(proxy, proxiedMethod, args, SequenceNumber.next(), - proxiedMethod, new LocationImpl())); + return handler.handle(new ProxyInvocation(proxy, method, args, new DelegatingMethod + (method), SequenceNumber.next(), new LocationImpl())); } public MockHandler getHandler() { @@ -73,51 +77,107 @@ final class InvocationHandlerAdapter implements InvocationHandler { && method.getParameterTypes().length == 0; } - private static class ProxiedMethod implements MockitoMethod, RealMethod { + /** + * Invocation on a proxy + */ + private class ProxyInvocation implements Invocation { + private final Object proxy; private final Method method; - - ProxiedMethod(Method method) { + private final Object[] rawArgs; + private final int sequenceNumber; + private final Location location; + private final Object[] args; + + private StubInfo stubInfo; + private boolean isIgnoredForVerification; + private boolean verified; + + private ProxyInvocation(Object proxy, Method method, Object[] rawArgs, DelegatingMethod + mockitoMethod, int sequenceNumber, Location location) { + this.rawArgs = rawArgs; + this.proxy = proxy; this.method = method; + this.sequenceNumber = sequenceNumber; + this.location = location; + args = ArgumentsProcessor.expandArgs(mockitoMethod, rawArgs); } @Override - public String getName() { - return method.getName(); + public Object getMock() { + return proxy; } @Override - public Class<?> getReturnType() { - return method.getReturnType(); + public Method getMethod() { + return method; } @Override - public Class<?>[] getParameterTypes() { - return method.getParameterTypes(); + public Object[] getArguments() { + return args; } @Override - public Class<?>[] getExceptionTypes() { - return method.getExceptionTypes(); + public <T> T getArgument(int index) { + return (T)args[index]; } @Override - public boolean isVarArgs() { - return method.isVarArgs(); + public Object callRealMethod() throws Throwable { + if (Modifier.isAbstract(method.getModifiers())) { + throw cannotCallAbstractRealMethod(); + } + return ProxyBuilder.callSuper(proxy, method, rawArgs); } @Override - public Method getJavaMethod() { - return method; + public boolean isVerified() { + return verified || isIgnoredForVerification; + } + + @Override + public int getSequenceNumber() { + return sequenceNumber; + } + + @Override + public Location getLocation() { + return location; + } + + @Override + public Object[] getRawArguments() { + return rawArgs; + } + + @Override + public Class<?> getRawReturnType() { + return method.getReturnType(); + } + + @Override + public void markVerified() { + verified = true; + } + + @Override + public StubInfo stubInfo() { + return stubInfo; + } + + @Override + public void markStubbed(StubInfo stubInfo) { + this.stubInfo = stubInfo; } @Override - public Object invoke(Object target, Object[] arguments) throws Throwable { - return ProxyBuilder.callSuper(target, method, arguments); + public boolean isIgnoredForVerification() { + return isIgnoredForVerification; } @Override - public boolean isAbstract() { - return Modifier.isAbstract(method.getModifiers()); + public void ignoreForVerification() { + isIgnoredForVerification = true; } } } diff --git a/dexmaker-tests/src/androidTest/java/com/android/dx/DexMakerTest.java b/dexmaker-tests/src/androidTest/java/com/android/dx/DexMakerTest.java index e955d82..f2b457f 100644 --- a/dexmaker-tests/src/androidTest/java/com/android/dx/DexMakerTest.java +++ b/dexmaker-tests/src/androidTest/java/com/android/dx/DexMakerTest.java @@ -572,6 +572,69 @@ public final class DexMakerTest { } @Test + public void testBranchingZ() throws Exception { + Method lt = branchingZMethod(Comparison.LT); + assertEquals(Boolean.TRUE, lt.invoke(null, -1)); + assertEquals(Boolean.FALSE, lt.invoke(null, 0)); + assertEquals(Boolean.FALSE, lt.invoke(null, 1)); + + Method le = branchingZMethod(Comparison.LE); + assertEquals(Boolean.TRUE, le.invoke(null, -1)); + assertEquals(Boolean.TRUE, le.invoke(null, 0)); + assertEquals(Boolean.FALSE, le.invoke(null, 1)); + + Method eq = branchingZMethod(Comparison.EQ); + assertEquals(Boolean.FALSE, eq.invoke(null, -1)); + assertEquals(Boolean.TRUE, eq.invoke(null, 0)); + assertEquals(Boolean.FALSE, eq.invoke(null, 1)); + + Method ge = branchingZMethod(Comparison.GE); + assertEquals(Boolean.FALSE, ge.invoke(null, -1)); + assertEquals(Boolean.TRUE, ge.invoke(null, 0)); + assertEquals(Boolean.TRUE, ge.invoke(null, 1)); + + Method gt = branchingZMethod(Comparison.GT); + assertEquals(Boolean.FALSE, gt.invoke(null, -1)); + assertEquals(Boolean.FALSE, gt.invoke(null, 0)); + assertEquals(Boolean.TRUE, gt.invoke(null, 1)); + + Method ne = branchingZMethod(Comparison.NE); + assertEquals(Boolean.TRUE, ne.invoke(null, -1)); + assertEquals(Boolean.FALSE, ne.invoke(null, 0)); + assertEquals(Boolean.TRUE, ne.invoke(null, 1)); + } + + private Method branchingZMethod(Comparison comparison) throws Exception { + /* + * public static boolean call(int localA) { + * if (a comparison 0) { + * return true; + * } + * return false; + * } + */ + reset(); + MethodId<?, Boolean> methodId = GENERATED.getMethod( + TypeId.BOOLEAN, "call", TypeId.INT); + Code code = dexMaker.declare(methodId, PUBLIC | STATIC); + Local<Integer> localA = code.getParameter(0, TypeId.INT); + Local<Boolean> result = code.newLocal(TypeId.get(boolean.class)); + Label afterIf = new Label(); + Label ifBody = new Label(); + code.compareZ(comparison, ifBody, localA); + code.jump(afterIf); + + code.mark(ifBody); + code.loadConstant(result, true); + code.returnValue(result); + + code.mark(afterIf); + code.loadConstant(result, false); + code.returnValue(result); + return getMethod(); + } + + @Test public void testCastIntegerToInteger() throws Exception { Method intToLong = numericCastingMethod(int.class, long.class); assertEquals(0x0000000000000000L, intToLong.invoke(null, 0x00000000)); @@ -1103,6 +1166,74 @@ public final class DexMakerTest { } @Test + public void testStaticInitializer() throws Exception { + reset(); + + StaticFieldSpec<?>[] fields = new StaticFieldSpec[] { + new StaticFieldSpec<>(boolean.class, "booleanValue", true), + new StaticFieldSpec<>(byte.class, "byteValue", Byte.MIN_VALUE), + new StaticFieldSpec<>(short.class, "shortValue", Short.MAX_VALUE), + new StaticFieldSpec<>(int.class, "intValue", Integer.MIN_VALUE), + new StaticFieldSpec<>(long.class, "longValue", Long.MAX_VALUE), + new StaticFieldSpec<>(float.class, "floatValue", Float.MIN_VALUE), + new StaticFieldSpec<>(double.class, "doubleValue", Double.MAX_VALUE), + new StaticFieldSpec<>(String.class, "stringValue", "qwerty"), + }; + + MethodId<?, Void> clinit = GENERATED.getStaticInitializer(); + assertTrue(clinit.isStaticInitializer()); + + Code code = dexMaker.declare(clinit, Modifier.STATIC); + + for (StaticFieldSpec<?> field : fields) { + field.createLocal(code); + } + + for (StaticFieldSpec<?> field : fields) { + field.initializeField(code); + } + + code.returnVoid(); + + Class<?> generated = generateAndLoad(); + for (StaticFieldSpec<?> fieldSpec : fields) { + Field field = generated.getDeclaredField(fieldSpec.name); + assertEquals(StaticFieldSpec.MODIFIERS, field.getModifiers()); + assertEquals(fieldSpec.value, field.get(null)); + } + } + + private class StaticFieldSpec<T> { + Class<T> type; + TypeId<T> typeId; + String name; + T value; + FieldId<?, T> fieldId; + Local<T> local; + + static final int MODIFIERS = Modifier.PUBLIC | Modifier.STATIC | Modifier.FINAL; + + public StaticFieldSpec(Class<T> type, String name, T value) { + this.type = type; + this.name = name; + this.value = value; + + typeId = TypeId.get(type); + fieldId = GENERATED.getField(typeId, name); + dexMaker.declare(fieldId, MODIFIERS, null); + } + + public void createLocal(Code code) { + local = code.newLocal(typeId); + } + + public void initializeField(Code code) { + code.loadConstant(local, value); + code.sput(fieldId, local); + } + } + + @Test public void testTypeCast() throws Exception { /* * public static String call(Object o) { @@ -1881,8 +2012,9 @@ public final class DexMakerTest { dexMaker.declare(GENERATED, "Generated.java", PUBLIC, TypeId.OBJECT); addMethodToDexMakerGenerator(TypeId.INT, defaultMethodName, TypeId.INT); generateAndLoad(); + int numFiles = getDataDirectory().listFiles().length; // DexMaker writes two files to disk at a time: Generated_XXXX.jar and Generated_XXXX.dex. - assertEquals(origSize + 2, getDataDirectory().listFiles().length); + assertTrue(origSize < numFiles); long lastModified = getJarFiles()[0].lastModified(); @@ -1891,7 +2023,7 @@ public final class DexMakerTest { dexMaker.declare(GENERATED, "Generated.java", PUBLIC, TypeId.OBJECT); addMethodToDexMakerGenerator(TypeId.INT, defaultMethodName, TypeId.INT); generateAndLoad(); - assertEquals(origSize + 2, getDataDirectory().listFiles().length); + assertEquals(numFiles, getDataDirectory().listFiles().length); assertEquals(lastModified, getJarFiles()[0].lastModified()); // Create new dexmaker generators with different params. @@ -1899,20 +2031,23 @@ public final class DexMakerTest { dexMaker.declare(GENERATED, "Generated.java", PUBLIC, TypeId.OBJECT); addMethodToDexMakerGenerator(TypeId.INT, defaultMethodName, TypeId.DOUBLE); generateAndLoad(); - assertEquals(origSize + 4, getDataDirectory().listFiles().length); + assertTrue(numFiles < getDataDirectory().listFiles().length); + numFiles = getDataDirectory().listFiles().length; dexMaker = new DexMaker(); dexMaker.declare(GENERATED, "Generated.java", PUBLIC, TypeId.OBJECT); addMethodToDexMakerGenerator(TypeId.INT, defaultMethodName, TypeId.INT, TypeId.DOUBLE); generateAndLoad(); - assertEquals(origSize + 6, getDataDirectory().listFiles().length); + assertTrue(numFiles < getDataDirectory().listFiles().length); + numFiles = getDataDirectory().listFiles().length; // Create new dexmaker generator with different return types. dexMaker = new DexMaker(); dexMaker.declare(GENERATED, "Generated.java", PUBLIC, TypeId.OBJECT); addMethodToDexMakerGenerator(TypeId.DOUBLE, defaultMethodName, TypeId.INT); generateAndLoad(); - assertEquals(origSize + 8, getDataDirectory().listFiles().length); + assertTrue(numFiles < getDataDirectory().listFiles().length); + numFiles = getDataDirectory().listFiles().length; // Create new dexmaker generators with multiple methods. dexMaker = new DexMaker(); @@ -1920,35 +2055,38 @@ public final class DexMakerTest { addMethodToDexMakerGenerator(TypeId.INT, defaultMethodName, TypeId.INT); addMethodToDexMakerGenerator(TypeId.INT, defaultMethodName, TypeId.INT, TypeId.BOOLEAN); // new method generateAndLoad(); - assertEquals(origSize + 10, getDataDirectory().listFiles().length); + assertTrue(numFiles < getDataDirectory().listFiles().length); + numFiles = getDataDirectory().listFiles().length; dexMaker = new DexMaker(); dexMaker.declare(GENERATED, "Generated.java", PUBLIC, TypeId.OBJECT); addMethodToDexMakerGenerator(TypeId.INT, defaultMethodName, TypeId.INT, TypeId.BOOLEAN); addMethodToDexMakerGenerator(TypeId.INT, defaultMethodName, TypeId.INT); generateAndLoad(); - assertEquals(origSize + 10, getDataDirectory().listFiles().length); // should already be cached. + assertEquals(numFiles, getDataDirectory().listFiles().length); // should already be cached. dexMaker = new DexMaker(); dexMaker.declare(GENERATED, "Generated.java", PUBLIC, TypeId.OBJECT); addMethodToDexMakerGenerator(TypeId.INT, defaultMethodName, TypeId.INT); addMethodToDexMakerGenerator(TypeId.INT, defaultMethodName, TypeId.INT, TypeId.INT, TypeId.BOOLEAN); // new method generateAndLoad(); - assertEquals(origSize + 12, getDataDirectory().listFiles().length); + assertTrue(numFiles < getDataDirectory().listFiles().length); + numFiles = getDataDirectory().listFiles().length; dexMaker = new DexMaker(); dexMaker.declare(GENERATED, "Generated.java", PUBLIC, TypeId.OBJECT); addMethodToDexMakerGenerator(TypeId.INT, defaultMethodName, TypeId.INT); addMethodToDexMakerGenerator(TypeId.INT, defaultMethodName, TypeId.INT, TypeId.INT); // new method generateAndLoad(); - assertEquals(origSize + 14, getDataDirectory().listFiles().length); + assertTrue(numFiles < getDataDirectory().listFiles().length); + numFiles = getDataDirectory().listFiles().length; dexMaker = new DexMaker(); dexMaker.declare(GENERATED, "Generated.java", PUBLIC, TypeId.OBJECT); addMethodToDexMakerGenerator(TypeId.INT, "differentName", TypeId.INT); // new method addMethodToDexMakerGenerator(TypeId.INT, defaultMethodName, TypeId.INT, TypeId.BOOLEAN); generateAndLoad(); - assertEquals(origSize + 16, getDataDirectory().listFiles().length); + assertTrue(numFiles < getDataDirectory().listFiles().length); } public static class BlankClassA {} @@ -1966,15 +2104,15 @@ public final class DexMakerTest { addMethodToDexMakerGenerator(TypeId.INT, defaultMethodName, TypeId.INT); generateAndLoad(); // DexMaker writes two files to disk at a time: Generated_XXXX.jar and Generated_XXXX.dex. - assertEquals(origSize + 2, getDataDirectory().listFiles().length); + int numFiles = getDataDirectory().listFiles().length; + assertTrue(origSize < numFiles); // Create new dexmaker generator with BlankClassB as supertype. dexMaker = new DexMaker(); dexMaker.declare(GENERATED, "Generated.java", PUBLIC, TypeId.get(BlankClassB.class)); addMethodToDexMakerGenerator(TypeId.INT, defaultMethodName, TypeId.INT); generateAndLoad(); - assertEquals(origSize + 4, getDataDirectory().listFiles().length); - + assertTrue(numFiles < getDataDirectory().listFiles().length); } private void addMethodToDexMakerGenerator(TypeId<?> typeId, String methodName, TypeId<?>... params) throws Exception { @@ -2004,7 +2142,8 @@ public final class DexMakerTest { addConstructorToDexMakerGenerator(TypeId.INT); generateAndLoad(); // DexMaker writes two files to disk at a time: Generated_XXXX.jar and Generated_XXXX.dex. - assertEquals(origSize + 2, getDataDirectory().listFiles().length); + int numFiles = getDataDirectory().listFiles().length; + assertTrue(origSize < numFiles); long lastModified = getJarFiles()[0].lastModified(); @@ -2012,7 +2151,7 @@ public final class DexMakerTest { dexMaker.declare(GENERATED, "Generated.java", PUBLIC, TypeId.OBJECT); addConstructorToDexMakerGenerator(TypeId.INT); generateAndLoad(); - assertEquals(origSize + 2, getDataDirectory().listFiles().length); + assertEquals(numFiles, getDataDirectory().listFiles().length); assertEquals(lastModified, getJarFiles()[0].lastModified()); // Create new dexmaker generator with Generated(boolean) constructor. @@ -2020,7 +2159,8 @@ public final class DexMakerTest { dexMaker.declare(GENERATED, "Generated.java", PUBLIC, TypeId.OBJECT); addConstructorToDexMakerGenerator(TypeId.BOOLEAN); generateAndLoad(); - assertEquals(origSize + 4, getDataDirectory().listFiles().length); + assertTrue(numFiles < getDataDirectory().listFiles().length); + numFiles = getDataDirectory().listFiles().length; // Create new dexmaker generator with multiple constructors. dexMaker = new DexMaker(); @@ -2028,7 +2168,8 @@ public final class DexMakerTest { addConstructorToDexMakerGenerator(TypeId.INT); addConstructorToDexMakerGenerator(TypeId.BOOLEAN); generateAndLoad(); - assertEquals(origSize + 6, getDataDirectory().listFiles().length); + assertTrue(numFiles < getDataDirectory().listFiles().length); + numFiles = getDataDirectory().listFiles().length; // Ensure that order of constructors does not affect caching decision. dexMaker = new DexMaker(); @@ -2036,7 +2177,7 @@ public final class DexMakerTest { addConstructorToDexMakerGenerator(TypeId.BOOLEAN); addConstructorToDexMakerGenerator(TypeId.INT); generateAndLoad(); - assertEquals(origSize + 6, getDataDirectory().listFiles().length); + assertEquals(numFiles, getDataDirectory().listFiles().length); } private void addConstructorToDexMakerGenerator(TypeId<?>... params) throws Exception { diff --git a/dexmaker-tests/src/androidTest/java/com/android/dx/stock/ProxyBuilderTest.java b/dexmaker-tests/src/androidTest/java/com/android/dx/stock/ProxyBuilderTest.java index 193a6d1..7127ec5 100644 --- a/dexmaker-tests/src/androidTest/java/com/android/dx/stock/ProxyBuilderTest.java +++ b/dexmaker-tests/src/androidTest/java/com/android/dx/stock/ProxyBuilderTest.java @@ -27,7 +27,9 @@ 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; @@ -1106,7 +1108,8 @@ public class ProxyBuilderTest { .build(); assertEquals(1, proxyA.foo()); assertEquals("bar", proxyA.bar()); - assertEquals(2, versionedDxDir.listFiles().length); + int numFiles = versionedDxDir.listFiles().length; + assertTrue(numFiles > 0); ConcreteClassB proxyB = ProxyBuilder.forClass(ConcreteClassB.class) .handler(new InvokeSuperHandler()) @@ -1114,6 +1117,61 @@ public class ProxyBuilderTest { .build(); assertEquals(0, proxyB.foo()); assertEquals("bahhr", proxyB.bar()); - assertEquals(4, versionedDxDir.listFiles().length); + 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<Method> 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() { + 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()); + } } diff --git a/dexmaker/build.gradle b/dexmaker/build.gradle index eb337ec..73c9344 100644 --- a/dexmaker/build.gradle +++ b/dexmaker/build.gradle @@ -13,6 +13,4 @@ repositories { dependencies { compile 'com.jakewharton.android.repackaged:dalvik-dx:7.1.0_r7' - - testCompile 'junit:junit:4.12' } diff --git a/dexmaker/src/main/java/com/android/dx/Code.java b/dexmaker/src/main/java/com/android/dx/Code.java index 721d659..715d2b4 100644 --- a/dexmaker/src/main/java/com/android/dx/Code.java +++ b/dexmaker/src/main/java/com/android/dx/Code.java @@ -534,13 +534,24 @@ public final class Code { */ public <T> void compare(Comparison comparison, Label trueLabel, Local<T> a, Local<T> b) { adopt(trueLabel); - // TODO: ops to compare with zero/null: just omit the 2nd local in StdTypeList.make() Rop rop = comparison.rop(StdTypeList.make(a.type.ropType, b.type.ropType)); addInstruction(new PlainInsn(rop, sourcePosition, null, RegisterSpecList.make(a.spec(), b.spec())), trueLabel); } /** + * Check if an int or reference equals to zero. If the comparison is true, + * execution jumps to {@code trueLabel}. If it is false, execution continues to + * the next instruction. + */ + public <T> void compareZ(Comparison comparison, Label trueLabel, Local<?> a) { + adopt(trueLabel); + Rop rop = comparison.rop(StdTypeList.make(a.type.ropType)); + addInstruction(new PlainInsn(rop, sourcePosition, null, + RegisterSpecList.make(a.spec())), trueLabel); + } + + /** * Compare floats or doubles. This stores -1 in {@code target} if {@code * a < b}, 0 in {@code target} if {@code a == b} and 1 in target if {@code * a > b}. This stores {@code nanValue} in {@code target} if either value @@ -576,7 +587,7 @@ public final class Code { * Copies the value in instance field {@code fieldId} of {@code instance} to * {@code target}. */ - public <D, V> void iget(FieldId<D, V> fieldId, Local<V> target, Local<D> instance) { + public <D, V> void iget(FieldId<D, ? extends V> fieldId, Local<V> target, Local<D> instance) { addInstruction(new ThrowingCstInsn(Rops.opGetField(target.type.ropType), sourcePosition, RegisterSpecList.make(instance.spec()), catches, fieldId.constant)); moveResult(target, true); @@ -586,7 +597,7 @@ public final class Code { * Copies the value in {@code source} to the instance field {@code fieldId} * of {@code instance}. */ - public <D, V> void iput(FieldId<D, V> fieldId, Local<D> instance, Local<V> source) { + public <D, V> void iput(FieldId<D, V> fieldId, Local<? extends D> instance, Local<? extends V> source) { addInstruction(new ThrowingCstInsn(Rops.opPutField(source.type.ropType), sourcePosition, RegisterSpecList.make(source.spec(), instance.spec()), catches, fieldId.constant)); } @@ -594,7 +605,7 @@ public final class Code { /** * Copies the value in the static field {@code fieldId} to {@code target}. */ - public <V> void sget(FieldId<?, V> fieldId, Local<V> target) { + public <V> void sget(FieldId<?, ? extends V> fieldId, Local<V> target) { addInstruction(new ThrowingCstInsn(Rops.opGetStatic(target.type.ropType), sourcePosition, RegisterSpecList.EMPTY, catches, fieldId.constant)); moveResult(target, true); @@ -603,7 +614,7 @@ public final class Code { /** * Copies the value in {@code source} to the static field {@code fieldId}. */ - public <V> void sput(FieldId<?, V> fieldId, Local<V> source) { + public <V> void sput(FieldId<?, V> fieldId, Local<? extends V> source) { addInstruction(new ThrowingCstInsn(Rops.opPutStatic(source.type.ropType), sourcePosition, RegisterSpecList.make(source.spec()), catches, fieldId.constant)); } diff --git a/dexmaker/src/main/java/com/android/dx/DexMaker.java b/dexmaker/src/main/java/com/android/dx/DexMaker.java index ee8d722..f10ad8e 100644 --- a/dexmaker/src/main/java/com/android/dx/DexMaker.java +++ b/dexmaker/src/main/java/com/android/dx/DexMaker.java @@ -31,14 +31,12 @@ import com.android.dx.rop.code.RopMethod; import com.android.dx.rop.cst.CstString; import com.android.dx.rop.cst.CstType; import com.android.dx.rop.type.StdTypeList; -import com.android.dx.stock.ProxyBuilder; + import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Modifier; -import dalvik.system.DexClassLoader; - import java.util.Arrays; import java.util.Iterator; import java.util.LinkedHashMap; @@ -199,6 +197,7 @@ import static java.lang.reflect.Modifier.STATIC; */ public final class DexMaker { private final Map<TypeId<?>, TypeDeclaration> types = new LinkedHashMap<>(); + private ClassLoader sharedClassLoader; /** * Creates a new {@code DexMaker} instance, which can be used to create a @@ -269,7 +268,7 @@ public final class DexMaker { flags = (flags & ~Modifier.SYNCHRONIZED) | AccessFlags.ACC_DECLARED_SYNCHRONIZED; } - if (method.isConstructor()) { + if (method.isConstructor() || method.isStaticInitializer()) { flags |= ACC_CONSTRUCTOR; } @@ -357,15 +356,16 @@ public final class DexMaker { return "Generated_" + checksum +".jar"; } - private ClassLoader generateClassLoader(ClassLoader classLoader, File result, File dexCache, - ClassLoader parent) { + public void setSharedClassLoader(ClassLoader classLoader) { + this.sharedClassLoader = classLoader; + } + + private ClassLoader generateClassLoader(File result, File dexCache, ClassLoader parent) { try { - boolean shareClassLoader = Boolean.parseBoolean(System.getProperty( - "dexmaker.share_classloader", "false")); - if (shareClassLoader) { - ClassLoader loader = parent != null ? parent : classLoader; + if (sharedClassLoader != null) { + ClassLoader loader = parent != null ? parent : sharedClassLoader; loader.getClass().getMethod("addDexPath", String.class).invoke(loader, - result.getPath()); + result.getPath()); return loader; } else { return (ClassLoader) Class.forName("dalvik.system.DexClassLoader") @@ -411,11 +411,6 @@ public final class DexMaker { * application's private data dir. */ public ClassLoader generateAndLoad(ClassLoader parent, File dexCache) throws IOException { - return generateAndLoad(parent, parent, dexCache); - } - - public ClassLoader generateAndLoad(ClassLoader classLoader, ClassLoader parent, File dexCache) - throws IOException { if (dexCache == null) { String property = System.getProperty("dexmaker.dexcache"); if (property != null) { @@ -433,7 +428,7 @@ public final class DexMaker { // Check that the file exists. If it does, return a DexClassLoader and skip all // the dex bytecode generation. if (result.exists()) { - return generateClassLoader(classLoader, result, dexCache, parent); + return generateClassLoader(result, dexCache, parent); } byte[] dex = generate(); @@ -453,7 +448,7 @@ public final class DexMaker { jarOut.write(dex); jarOut.closeEntry(); jarOut.close(); - return generateClassLoader(classLoader, result, dexCache, parent); + return generateClassLoader(result, dexCache, parent); } private static class TypeDeclaration { diff --git a/dexmaker/src/main/java/com/android/dx/MethodId.java b/dexmaker/src/main/java/com/android/dx/MethodId.java index 746d73e..d891b97 100644 --- a/dexmaker/src/main/java/com/android/dx/MethodId.java +++ b/dexmaker/src/main/java/com/android/dx/MethodId.java @@ -66,7 +66,15 @@ public final class MethodId<D, R> { } /** - * Returns the method's name. This is "<init>" if this is a constructor. + * Returns true if this method is the static initializer for its declaring class. + */ + public boolean isStaticInitializer() { + return name.equals("<clinit>"); + } + + /** + * Returns the method's name. This is "<init>" if this is a constructor + * or "<clinit>" if a static initializer */ public String getName() { return name; diff --git a/dexmaker/src/main/java/com/android/dx/TypeId.java b/dexmaker/src/main/java/com/android/dx/TypeId.java index de96028..7131f33 100644 --- a/dexmaker/src/main/java/com/android/dx/TypeId.java +++ b/dexmaker/src/main/java/com/android/dx/TypeId.java @@ -123,6 +123,10 @@ public final class TypeId<T> { return new MethodId<>(this, VOID, "<init>", new TypeList(parameters)); } + public MethodId<T, Void> getStaticInitializer() { + return new MethodId<>(this, VOID, "<clinit>", new TypeList(new TypeId[0])); + } + public <R> MethodId<T, R> getMethod(TypeId<R> returnType, String name, TypeId<?>... parameters) { return new MethodId<>(this, returnType, name, new TypeList(parameters)); } diff --git a/dexmaker/src/main/java/com/android/dx/stock/ProxyBuilder.java b/dexmaker/src/main/java/com/android/dx/stock/ProxyBuilder.java index b618c84..1363894 100644 --- a/dexmaker/src/main/java/com/android/dx/stock/ProxyBuilder.java +++ b/dexmaker/src/main/java/com/android/dx/stock/ProxyBuilder.java @@ -140,6 +140,8 @@ public final class ProxyBuilder<T> { private Class<?>[] constructorArgTypes = new Class[0]; private Object[] constructorArgValues = new Object[0]; private Set<Class<?>> interfaces = new HashSet<>(); + private Method[] methods; + private boolean sharedClassLoader; private ProxyBuilder(Class<T> clazz) { baseClass = clazz; @@ -195,6 +197,16 @@ public final class ProxyBuilder<T> { return this; } + public ProxyBuilder<T> onlyMethods(Method[] methods) { + this.methods = methods; + return this; + } + + public ProxyBuilder<T> withSharedClassLoader() { + this.sharedClassLoader = true; + return this; + } + /** * Create a new instance of the class to proxy. * @@ -249,12 +261,10 @@ public final class ProxyBuilder<T> { @SuppressWarnings("unchecked") Class<? extends T> proxyClass = (Class) generatedProxyClasses.get(baseClass); if (proxyClass != null) { - boolean shareClassLoader = Boolean.parseBoolean(System.getProperty( - "dexmaker.share_classloader", "false")); boolean validClassLoader; - if (shareClassLoader) { - ClassLoader parent = parentClassLoader != null ? parentClassLoader - : baseClass.getClassLoader(); + if (sharedClassLoader) { + ClassLoader parent = parentClassLoader != null ? parentClassLoader : baseClass + .getClassLoader(); validClassLoader = proxyClass.getClassLoader() == parent; } else { validClassLoader = proxyClass.getClassLoader().getParent() == parentClassLoader; @@ -270,11 +280,28 @@ public final class ProxyBuilder<T> { TypeId<? extends T> generatedType = TypeId.get("L" + generatedName + ";"); TypeId<T> superType = TypeId.get(baseClass); generateConstructorsAndFields(dexMaker, generatedType, superType, baseClass); - Method[] methodsToProxy = getMethodsToProxyRecursive(); + + Method[] methodsToProxy; + if (methods == null) { + methodsToProxy = getMethodsToProxyRecursive(); + } else { + methodsToProxy = methods; + } + + // Sort the results array so that they are in a deterministic fashion. + Arrays.sort(methodsToProxy, new Comparator<Method>() { + @Override + public int compare(Method method1, Method method2) { + return method1.toString().compareTo(method2.toString()); + } + }); + generateCodeForAllMethods(dexMaker, generatedType, methodsToProxy, superType); dexMaker.declare(generatedType, generatedName + ".generated", PUBLIC, superType, getInterfacesAsTypeIds()); - ClassLoader classLoader = dexMaker.generateAndLoad(baseClass.getClassLoader(), - parentClassLoader, dexCache); + if (sharedClassLoader) { + dexMaker.setSharedClassLoader(baseClass.getClassLoader()); + } + ClassLoader classLoader = dexMaker.generateAndLoad(parentClassLoader, dexCache); try { proxyClass = loadClass(classLoader, generatedName); } catch (IllegalAccessError e) { @@ -654,22 +681,11 @@ public final class ProxyBuilder<T> { results[i++] = entry.originalMethod; } - // Sort the results array so that they are returned by this method - // in a deterministic fashion. - Arrays.sort(results, new Comparator<Method>() { - @Override - public int compare(Method method1, Method method2) { - return method1.toString().compareTo(method2.toString()); - } - }); - return results; } private void getMethodsToProxy(Set<MethodSetEntry> sink, Set<MethodSetEntry> seenFinalMethods, Class<?> c) { - boolean shareClassLoader = Boolean.parseBoolean(System.getProperty( - "dexmaker.share_classloader", "false")); for (Method method : c.getDeclaredMethods()) { if ((method.getModifiers() & Modifier.FINAL) != 0) { // Skip final methods, we can't override them. We @@ -687,8 +703,8 @@ public final class ProxyBuilder<T> { continue; } if (!Modifier.isPublic(method.getModifiers()) - && !Modifier.isProtected(method.getModifiers()) - && (!shareClassLoader || Modifier.isPrivate(method.getModifiers()))) { + && !Modifier.isProtected(method.getModifiers()) + && (!sharedClassLoader || Modifier.isPrivate(method.getModifiers()))) { // Skip private methods, since they are invoked through direct // invocation (as opposed to virtual). Therefore, it would not // be possible to intercept any private method defined inside @@ -820,11 +836,11 @@ public final class ProxyBuilder<T> { * another. For these purposes, we consider two methods to be equal if they have the same * name, return type, and parameter types. */ - private static class MethodSetEntry { - private final String name; - private final Class<?>[] paramTypes; - private final Class<?> returnType; - private final Method originalMethod; + public static class MethodSetEntry { + public final String name; + public final Class<?>[] paramTypes; + public final Class<?> returnType; + public final Method originalMethod; public MethodSetEntry(Method method) { originalMethod = method; diff --git a/update_source.sh b/update_source.sh index b38a072..ff7cc66 100755 --- a/update_source.sh +++ b/update_source.sh @@ -25,6 +25,10 @@ INCLUDE=" LICENSE dexmaker dexmaker-mockito + dexmaker-mockito-tests + dexmaker-mockito-inline + dexmaker-mockito-inline-dispatcher + dexmaker-mockito-inline-tests/src dexmaker-tests/src " @@ -36,7 +40,11 @@ trap "echo \"Removing temporary directory\"; rm -rf $working_dir" EXIT echo "Fetching Dexmaker source into $working_dir" git clone $SOURCE $working_dir/source -(cd $working_dir/source; git checkout $VERSION) +ORG_DIR=$(pwd) +cd $working_dir/source +git checkout $VERSION +SHA=$(git rev-parse $VERSION) +cd $ORG_DIR for include in ${INCLUDE}; do echo "Updating $include" @@ -52,11 +60,16 @@ done; # Move the dexmaker-tests AndroidManifest.xml into the correct position. mv dexmaker-tests/src/main/AndroidManifest.xml dexmaker-tests/AndroidManifest.xml +mv dexmaker-mockito-tests/src/main/AndroidManifest.xml dexmaker-mockito-tests/AndroidManifest.xml +mv dexmaker-mockito-inline-tests/src/main/AndroidManifest.xml dexmaker-mockito-inline-tests/AndroidManifest.xml + +# Remove 3rd party code +rm -r dexmaker-mockito-inline/external echo "Updating README.version" # Update the version. -perl -pi -e "s|^Version: .*$|Version: ${VERSION}|" "README.version" +perl -pi -e "s|^Version: .*$|Version: ${VERSION} (${SHA})|" "README.version" # Remove any documentation about local modifications. mv README.version README.tmp |