aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPhilip P. Moltmann <moltmann@google.com>2017-11-20 09:38:54 -0800
committerPhilip P. Moltmann <moltmann@google.com>2017-12-12 12:50:10 -0800
commit171f097997993b84053f643dc275ce66364315ca (patch)
treeff5169be265d1ee674c4bd7748d52ca15b5ce20a
parent61a444d2c7f6c6a81ca2bf54958b02b7cdc81c04 (diff)
downloaddexmaker-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
-rw-r--r--Android.bp90
-rw-r--r--README.version9
-rw-r--r--dexmaker-mockito-inline-dispatcher/build.gradle17
-rw-r--r--dexmaker-mockito-inline-dispatcher/src/main/AndroidManifest.xml3
-rw-r--r--dexmaker-mockito-inline-dispatcher/src/main/java/com/android/dx/mockito/inline/MockMethodDispatcher.java129
-rw-r--r--dexmaker-mockito-inline-tests/AndroidManifest.xml3
-rw-r--r--dexmaker-mockito-inline-tests/src/androidTest/java/com/android/dx/mockito/inline/tests/CleanStackTrace.java87
-rw-r--r--dexmaker-mockito-inline-tests/src/androidTest/java/com/android/dx/mockito/inline/tests/MockFinal.java226
-rw-r--r--dexmaker-mockito-inline-tests/src/androidTest/java/com/android/dx/mockito/inline/tests/MockNonPublic.java391
-rw-r--r--dexmaker-mockito-inline-tests/src/androidTest/java/com/android/dx/mockito/inline/tests/MultipleJvmtiAgentsInterference.java91
-rw-r--r--dexmaker-mockito-inline-tests/src/main/jni/multiplejvmtiagentsinterferenceagent/agent.cc151
-rw-r--r--dexmaker-mockito-inline/CMakeLists.txt33
-rw-r--r--dexmaker-mockito-inline/build.gradle33
-rw-r--r--dexmaker-mockito-inline/src/main/AndroidManifest.xml3
-rw-r--r--dexmaker-mockito-inline/src/main/java/com/android/dx/mockito/inline/ClassTransformer.java190
-rw-r--r--dexmaker-mockito-inline/src/main/java/com/android/dx/mockito/inline/DexmakerStackTraceCleaner.java51
-rw-r--r--dexmaker-mockito-inline/src/main/java/com/android/dx/mockito/inline/IllegalClassFormatException.java24
-rw-r--r--dexmaker-mockito-inline/src/main/java/com/android/dx/mockito/inline/InlineDexmakerMockMaker.java302
-rw-r--r--dexmaker-mockito-inline/src/main/java/com/android/dx/mockito/inline/InterceptedInvocation.java189
-rw-r--r--dexmaker-mockito-inline/src/main/java/com/android/dx/mockito/inline/InvocationHandlerAdapter.java226
-rw-r--r--dexmaker-mockito-inline/src/main/java/com/android/dx/mockito/inline/JvmtiAgent.java190
-rw-r--r--dexmaker-mockito-inline/src/main/java/com/android/dx/mockito/inline/MockFeatures.java23
-rw-r--r--dexmaker-mockito-inline/src/main/java/com/android/dx/mockito/inline/MockMethodAdvice.java341
-rw-r--r--dexmaker-mockito-inline/src/main/java/com/android/dx/mockito/inline/UnmodifiableClassException.java27
-rw-r--r--dexmaker-mockito-inline/src/main/jni/dexmakerjvmtiagent/agent.cc886
-rw-r--r--dexmaker-mockito-inline/src/main/resources/README.txt2
-rw-r--r--dexmaker-mockito-inline/src/main/resources/dispatcher.jarbin0 -> 2420 bytes
-rw-r--r--dexmaker-mockito-inline/src/main/resources/mockito-extensions/org.mockito.plugins.MockMaker1
-rw-r--r--dexmaker-mockito-inline/src/main/resources/mockito-extensions/org.mockito.plugins.StackTraceCleanerProvider1
-rw-r--r--dexmaker-mockito-tests/AndroidManifest.xml3
-rw-r--r--dexmaker-mockito-tests/build.gradle31
-rw-r--r--dexmaker-mockito-tests/src/androidTest/java/com/android/dx/mockito/tests/MockTests.java119
-rw-r--r--dexmaker-mockito/build.gradle2
-rw-r--r--dexmaker-mockito/src/main/java/com/android/dx/mockito/DexmakerMockMaker.java13
-rw-r--r--dexmaker-mockito/src/main/java/com/android/dx/mockito/InterceptedInvocation.java189
-rw-r--r--dexmaker-mockito/src/main/java/com/android/dx/mockito/InvocationHandlerAdapter.java110
-rw-r--r--dexmaker-tests/src/androidTest/java/com/android/dx/DexMakerTest.java177
-rw-r--r--dexmaker-tests/src/androidTest/java/com/android/dx/stock/ProxyBuilderTest.java62
-rw-r--r--dexmaker/build.gradle2
-rw-r--r--dexmaker/src/main/java/com/android/dx/Code.java21
-rw-r--r--dexmaker/src/main/java/com/android/dx/DexMaker.java31
-rw-r--r--dexmaker/src/main/java/com/android/dx/MethodId.java10
-rw-r--r--dexmaker/src/main/java/com/android/dx/TypeId.java4
-rw-r--r--dexmaker/src/main/java/com/android/dx/stock/ProxyBuilder.java68
-rwxr-xr-xupdate_source.sh17
45 files changed, 4467 insertions, 111 deletions
diff --git a/Android.bp b/Android.bp
index 8b0cc40..d61b3aa 100644
--- a/Android.bp
+++ b/Android.bp
@@ -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
new file mode 100644
index 0000000..6a5cf5f
--- /dev/null
+++ b/dexmaker-mockito-inline/src/main/resources/dispatcher.jar
Binary files differ
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 "&lt;init&gt;" 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 "&lt;init&gt;" if this is a constructor
+ * or "&lt;clinit&gt;" 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