Update dexmaker on AOSP to same version as on internal master
Bug: 109745050
Test: atest CtsMockingTestCases
CtsInlineMockingTestCases
CtsMockingDebuggableTestCases
CtsExtendedMockingTestCases
Change-Id: I8d33026843b9721bd7f597e0f87b85ecf88bd69c
Merged-In: I33c12311cb17366b936f4716c1bf89a5d2e18074
diff --git a/.gitignore b/.gitignore
deleted file mode 100644
index 98b50ee..0000000
--- a/.gitignore
+++ /dev/null
@@ -1,3 +0,0 @@
-# IntelliJ IDEA
-.idea/
-*.iml
diff --git a/Android.bp b/Android.bp
index 61b24ec..caaf02b 100644
--- a/Android.bp
+++ b/Android.bp
@@ -78,11 +78,8 @@
"libz",
],
- // As an NDK-based library we cannot depend on libopenjdkjvmti_headers.
include_dirs: [
"art/openjdkjvmti/include",
- // TODO Remove once upstream has updated to new slicer.
- "tools/dexter/slicer/export/slicer",
],
}
@@ -95,6 +92,15 @@
srcs: ["dexmaker-mockito-inline/src/main/jni/**/*.cc"],
}
+// Build agent for Dexmaker's extended MockMaker
+cc_library_shared {
+ name: "libstaticjvmtiagent",
+ defaults: [
+ "dexmaker_agent_defaults",
+ ],
+ srcs: ["dexmaker-mockito-inline-extended/src/main/jni/**/*.cc"],
+}
+
// Build agent for Dexmaker's inline tests
cc_library_shared {
name: "libmultiplejvmtiagentsinterferenceagent",
@@ -115,12 +121,113 @@
"mockito-api",
],
required: ["libdexmakerjvmtiagent"],
+}
- errorprone: {
- javacflags: [
- "-Xep:CollectionIncompatibleType:WARN"
- ],
- }
+// Build Dexmaker's extended MockMaker, a plugin to Mockito
+java_library_static {
+ name: "dexmaker-extended-mockmaker",
+ sdk_version: "current",
+ srcs: ["dexmaker-mockito-inline-extended/src/main/java/**/*.java"],
+
+ java_resource_dirs: ["dexmaker-mockito-inline/src/main/resources"],
+
+ libs: [
+ "dexmaker",
+ "mockito-api",
+ "dexmaker-inline-mockmaker",
+ ],
+ required: [
+ "libdexmakerjvmtiagent",
+ "libstaticjvmtiagent"
+ ],
+}
+
+// Provides mockito functionality for on-device tests. Does not allow stubbing of final or static
+// methods.
+java_library_static {
+ name: "mockito-target",
+ no_framework_libs: true,
+ static_libs: [
+ "mockito-target-minus-junit4",
+ "junit",
+ ],
+ sdk_version: "16",
+}
+
+// Same as mockito-target but does not bundle junit
+java_library_static {
+ name: "mockito-target-minus-junit4",
+ no_framework_libs: true,
+ static_libs: [
+ "mockito",
+ "dexmaker",
+ "dexmaker-mockmaker",
+ "objenesis",
+ ],
+ libs: ["junit"],
+ sdk_version: "16",
+
+ java_version: "1.7",
+}
+
+// Provides mockito functionality for on-device tests. Allows stubbing of final methods. Does not
+// allow stubbing of static methods.
+// Project depending on this also need to depend on the static JNI library libdexmakerjvmtiagent
+java_library_static {
+ name: "mockito-target-inline",
+ no_framework_libs: true,
+ static_libs: [
+ "mockito-target-inline-minus-junit4",
+ "junit",
+ ],
+ sdk_version: "current",
+}
+
+// Same as mockito-target-inline but does not bundle junit
+java_library_static {
+ name: "mockito-target-inline-minus-junit4",
+ no_framework_libs: true,
+ static_libs: [
+ "mockito",
+ "dexmaker",
+ "dexmaker-inline-mockmaker",
+ "objenesis",
+ ],
+ libs: ["junit"],
+ sdk_version: "current",
+
+ java_version: "1.7",
+}
+
+// Provides mockito functionality for on-device tests. Allows stubbing of final and static methods.
+// Stubbing static methods is not an official mockito API.
+// Project depending on this also need to depend on the static JNI libraries libstaticjvmtiagent and
+// libdexmakerjvmtiagent
+java_library_static {
+ name: "mockito-target-extended",
+ no_framework_libs: true,
+ static_libs: [
+ "mockito-target-extended-minus-junit4",
+ "junit",
+ ],
+ sdk_version: "current",
+}
+
+// Same as mockito-target-extended but does not bundle junit
+java_library_static {
+ name: "mockito-target-extended-minus-junit4",
+ no_framework_libs: true,
+ static_libs: [
+ "mockito",
+ "dexmaker",
+ "dexmaker-inline-mockmaker",
+ "dexmaker-extended-mockmaker",
+ "objenesis",
+ ],
+ libs: ["junit"],
+ sdk_version: "current",
+
+ java_version: "1.7",
}
java_import {
@@ -132,3 +239,63 @@
name: "dexmaker-dex-target",
jars: ["lib/libcore-dex-2.jar"],
}
+
+// dexmaker tests
+java_library_static {
+ name: "dexmaker-tests-lib",
+ sdk_version: "current",
+ srcs: ["dexmaker-tests/src/androidTest/java/**/*.java"],
+
+ libs: [
+ "android-support-test",
+ "dexmaker",
+ "junit",
+ ],
+}
+
+// dexmaker-mockito tests
+java_library_static {
+ name: "dexmaker-mockmaker-tests",
+ sdk_version: "current",
+ srcs: ["dexmaker-mockito-tests/src/main/java/**/*.java"],
+
+ libs: [
+ "android-support-test",
+ "dexmaker",
+ "mockito",
+ "junit",
+ "dexmaker-mockmaker",
+ ],
+}
+
+// dexmaker-mockito-inline tests
+java_library_static {
+ name: "dexmaker-inline-mockmaker-tests",
+ sdk_version: "current",
+ srcs: ["dexmaker-mockito-inline-tests/src/main/java/**/*.java"],
+
+ libs: [
+ "android-support-test",
+ "dexmaker",
+ "android-support-v4",
+ "mockito",
+ "junit",
+ "dexmaker-inline-mockmaker",
+ ],
+}
+
+// dexmaker-mockito-extended tests
+java_library_static {
+ name: "dexmaker-extended-mockmaker-tests",
+ sdk_version: "current",
+ srcs: ["dexmaker-mockito-inline-extended-tests/src/main/java/**/*.java"],
+
+ libs: [
+ "android-support-test",
+ "dexmaker",
+ "android-support-v4",
+ "mockito",
+ "junit",
+ "dexmaker-extended-mockmaker",
+ ],
+}
diff --git a/Android.mk b/Android.mk
index 8bf81fb..ca527bf 100644
--- a/Android.mk
+++ b/Android.mk
@@ -14,4 +14,23 @@
LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+# Build a test APK
+#
+# Run the tests as follows:
+# m -j DexmakerTests && \
+# adb install -r $OUT/testcases/DexmakerTests/DexmakerTests.apk && \
+# adb shell am instrument -w com.linkedin.dexmaker
+
+LOCAL_PACKAGE_NAME := DexmakerTests
+LOCAL_SDK_VERSION := current
+
+LOCAL_MODULE_TAGS := tests
+LOCAL_MODULE_PATH := $(TARGET_OUT_DATA_APPS)
+
+LOCAL_STATIC_JAVA_LIBRARIES := dexmaker-tests-lib dexmaker android-support-test junit
+
+include $(BUILD_PACKAGE)
+
include $(call all-makefiles-under, $(LOCAL_PATH))
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 2c88308..c7e65ea 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -1,12 +1,26 @@
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="com.google.dexmaker.tests" >
+<!--
+ Copyright (C) 2018 The Android Open Source Project
- <application>
- <uses-library android:name="android.test.runner" />
- </application>
+ 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.
+ -->
+<manifest package="com.linkedin.dexmaker"
+ xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+ <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
+
+ <application android:allowBackup="false" android:debuggable="true" />
<instrumentation android:name="android.support.test.runner.AndroidJUnitRunner"
- android:targetPackage="com.google.dexmaker.tests"
- android:label="Dexmaker Tests"/>
-
+ android:targetPackage="com.linkedin.dexmaker" />
</manifest>
diff --git a/README.version b/README.version
index 92d3d64..5a8b61a 100644
--- a/README.version
+++ b/README.version
@@ -1,5 +1,5 @@
URL: https://github.com/linkedin/dexmaker/
-Version: master (5fb49bba98647d7a0aeea0cbf91fd670c3ff552a)
+Version: 2.19.0 (8532233e653b5178ce1e70016987ff776e7149f4)
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,6 +9,7 @@
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:
- Allow to share classloader via dexmaker.share_classloader system property (I8c2490c3ec8e8582dc41c486f8f7a406bd635ebb)
- Allow 'Q' until we can replace the version check with a number based check
- Mark mocks as trusted (needs upstreaming)
+ Add ability to run dexmaker tests from within the source tree (I1b146841099b54f64d4a7dfe743b88717793619a)
+ Allow to share classloader via dexmaker.share_classloader system property (Ia73198937e2e505f3baa96486f378fb8dc62d6d5)
+ Do not read Build.VERSION to allow non-standard Android distributions (I0b647514a306da979f7fdf96d5e5f8ae5e7ec945)
+ Fix caching of shared class loader proxies (needs upstream, I33c12311cb17366b936f4716c1bf89a5d2e18074)
diff --git a/bug-8108255.patch b/bug-8108255.patch
deleted file mode 100644
index bdccc8e..0000000
--- a/bug-8108255.patch
+++ /dev/null
@@ -1,16 +0,0 @@
-diff -ur a/mockito/src/main/java/com/google/dexmaker/mockito/DexmakerMockMaker.java b/mockito/src/main/java/com/google/dexmaker/mockito/DexmakerMockMaker.java
---- a/mockito/src/main/java/com/google/dexmaker/mockito/DexmakerMockMaker.java 2013-01-07 11:27:13.000000000 -0800
-+++ b/mockito/src/main/java/com/google/dexmaker/mockito/DexmakerMockMaker.java 2013-02-15 11:27:44.000000000 -0800
-@@ -45,9 +45,10 @@
- Class[] classesToMock = new Class[extraInterfaces.length + 1];
- classesToMock[0] = typeToMock;
- System.arraycopy(extraInterfaces, 0, classesToMock, 1, extraInterfaces.length);
-+ ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
- @SuppressWarnings("unchecked") // newProxyInstance returns the type of typeToMock
-- T mock = (T) Proxy.newProxyInstance(typeToMock.getClassLoader(),
-- classesToMock, invocationHandler);
-+ T mock = (T) Proxy.newProxyInstance(contextClassLoader, classesToMock,
-+ invocationHandler);
- return mock;
-
- } else {
diff --git a/build.gradle b/build.gradle
deleted file mode 100644
index 2a0f877..0000000
--- a/build.gradle
+++ /dev/null
@@ -1,26 +0,0 @@
-apply plugin: 'java'
-
-sourceSets {
- main {
- java {
- srcDirs = [
- 'src/main/java',
- 'src/dx/java',
- 'src/mockito/java',
- ]
- }
- resources {
- srcDirs = ['src/mockito/resources']
- }
- }
-}
-
-jar {
- baseName 'dexmaker'
- classifier 'mockmaker'
-}
-
-dependencies {
- compile getAndroidPrebuilt('10')
- compile project(path: ':mockito', configuration: 'target')
-}
diff --git a/dexmaker-mockito-inline-dispatcher/build.gradle b/dexmaker-mockito-inline-dispatcher/build.gradle
index c9667c0..9ea6567 100644
--- a/dexmaker-mockito-inline-dispatcher/build.gradle
+++ b/dexmaker-mockito-inline-dispatcher/build.gradle
@@ -1,17 +1,13 @@
apply plugin: 'com.android.application'
android {
- compileSdkVersion 25
- buildToolsVersion "25.0.0"
-
- lintOptions {
- abortOnError false
- }
+ compileSdkVersion 28
+ buildToolsVersion '28.0.0'
defaultConfig {
- applicationId "com.android.dexmaker.mockito.inline.dispatcher"
- minSdkVersion 25
- targetSdkVersion 25
+ applicationId 'com.android.dexmaker.mockito.inline.dispatcher'
+ minSdkVersion 28
+ targetSdkVersion 28
versionName VERSION_NAME
}
-}
\ No newline at end of file
+}
diff --git a/dexmaker-mockito-inline-extended-tests/build.gradle b/dexmaker-mockito-inline-extended-tests/build.gradle
new file mode 100644
index 0000000..20a8e16
--- /dev/null
+++ b/dexmaker-mockito-inline-extended-tests/build.gradle
@@ -0,0 +1,53 @@
+buildscript {
+ repositories {
+ maven {
+ url "https://plugins.gradle.org/m2/"
+ }
+ }
+ dependencies {
+ classpath "net.ltgt.gradle:gradle-errorprone-plugin:0.0.13"
+ }
+}
+
+apply plugin: "net.ltgt.errorprone"
+apply plugin: 'com.android.library'
+
+android {
+ compileSdkVersion 28
+
+ android {
+ lintOptions {
+ disable 'InvalidPackage'
+ }
+ }
+
+ defaultConfig {
+ minSdkVersion 28
+ targetSdkVersion 28
+ versionName VERSION_NAME
+
+ testInstrumentationRunner 'android.support.test.runner.AndroidJUnitRunner'
+ }
+
+ compileOptions {
+ targetCompatibility 1.8
+ sourceCompatibility 1.8
+ }
+}
+
+repositories {
+ jcenter()
+ google()
+}
+
+dependencies {
+ implementation project(':dexmaker-mockito-inline-tests')
+ compileOnly project(':dexmaker-mockito-inline-extended')
+ androidTestImplementation project(':dexmaker-mockito-inline-extended')
+
+ implementation 'junit:junit:4.12'
+ implementation 'com.android.support.test:runner:1.0.1'
+ implementation 'com.android.support.test:rules:1.0.2'
+
+ api 'org.mockito:mockito-core:2.19.0', { exclude group: 'net.bytebuddy' }
+}
diff --git a/dexmaker-mockito-inline-extended-tests/src/main/AndroidManifest.xml b/dexmaker-mockito-inline-extended-tests/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..11f9513
--- /dev/null
+++ b/dexmaker-mockito-inline-extended-tests/src/main/AndroidManifest.xml
@@ -0,0 +1,9 @@
+<manifest package="com.android.dexmaker.mockito.inline.extended.tests"
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools">
+
+ <application android:debuggable="true"
+ tools:ignore="HardcodedDebugMode">
+ <activity android:name="com.android.dx.mockito.inline.extended.tests.EmptyActivity" />
+ </application>
+</manifest>
diff --git a/dexmaker-mockito-inline-extended-tests/src/main/java/com/android/dx/mockito/inline/extended/tests/EmptyActivity.java b/dexmaker-mockito-inline-extended-tests/src/main/java/com/android/dx/mockito/inline/extended/tests/EmptyActivity.java
new file mode 100644
index 0000000..77e1f5a
--- /dev/null
+++ b/dexmaker-mockito-inline-extended-tests/src/main/java/com/android/dx/mockito/inline/extended/tests/EmptyActivity.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2018 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.extended.tests;
+
+import android.app.Activity;
+
+public class EmptyActivity extends Activity {
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ }
+}
diff --git a/dexmaker-mockito-inline-extended-tests/src/main/java/com/android/dx/mockito/inline/extended/tests/MockInstanceUsingExtendedMockito.java b/dexmaker-mockito-inline-extended-tests/src/main/java/com/android/dx/mockito/inline/extended/tests/MockInstanceUsingExtendedMockito.java
new file mode 100644
index 0000000..c21bde5
--- /dev/null
+++ b/dexmaker-mockito-inline-extended-tests/src/main/java/com/android/dx/mockito/inline/extended/tests/MockInstanceUsingExtendedMockito.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2018 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.extended.tests;
+
+import com.android.dx.mockito.inline.extended.ExtendedMockito;
+import com.android.dx.mockito.inline.extended.StaticInOrder;
+import com.android.dx.mockito.inline.extended.StaticMockitoSession;
+
+import org.junit.Test;
+import org.mockito.Mock;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.mockito.ArgumentMatchers.eq;
+
+public class MockInstanceUsingExtendedMockito {
+ @Mock
+ private TestClass mockField;
+
+ public static class TestClass {
+ public String echo(String arg) {
+ return arg;
+ }
+ }
+
+ @Test
+ public void mockClass() throws Exception {
+ TestClass t = mock(TestClass.class);
+
+ assertNull(t.echo("mocked"));
+
+ when(t.echo(eq("stubbed"))).thenReturn("B");
+ assertEquals("B", t.echo("stubbed"));
+ verify(t).echo("mocked");
+ verify(t).echo("stubbed");
+ }
+
+ @Test
+ public void useMockitoSession() throws Exception {
+ StaticMockitoSession session = mockitoSession().initMocks(this).startMocking();
+ try {
+ assertNull(mockField.echo("mocked"));
+
+ when(mockField.echo(eq("stubbed"))).thenReturn("B");
+ assertEquals("B", mockField.echo("stubbed"));
+ verify(mockField).echo("mocked");
+ verify(mockField).echo("stubbed");
+ } finally {
+ session.finishMocking();
+ }
+ }
+
+ @Test
+ public void verifyInOrder() throws Exception {
+ TestClass t = mock(TestClass.class);
+
+ assertNull(t.echo("mocked"));
+
+ when(t.echo(eq("stubbed"))).thenReturn("B");
+ assertEquals("B", t.echo("stubbed"));
+
+ StaticInOrder inOrder = ExtendedMockito.inOrder(t);
+ inOrder.verify(t).echo("mocked");
+ inOrder.verify(t).echo("stubbed");
+ }
+
+ @Test
+ public void mockClassUsingDoReturn() throws Exception {
+ TestClass t = mock(TestClass.class);
+
+ assertNull(t.echo("mocked"));
+
+ doReturn("B").when(t).echo(eq("stubbed"));
+ assertEquals("B", t.echo("stubbed"));
+ verify(t).echo("mocked");
+ verify(t).echo("stubbed");
+ }
+}
diff --git a/dexmaker-mockito-inline-extended-tests/src/main/java/com/android/dx/mockito/inline/extended/tests/MockStatic.java b/dexmaker-mockito-inline-extended-tests/src/main/java/com/android/dx/mockito/inline/extended/tests/MockStatic.java
new file mode 100644
index 0000000..3d1671e
--- /dev/null
+++ b/dexmaker-mockito-inline-extended-tests/src/main/java/com/android/dx/mockito/inline/extended/tests/MockStatic.java
@@ -0,0 +1,400 @@
+/*
+ * 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.extended.tests;
+
+import android.content.ContentResolver;
+import android.provider.Settings;
+import android.support.test.InstrumentationRegistry;
+
+import org.junit.Test;
+import org.mockito.MockingDetails;
+import org.mockito.MockitoSession;
+import org.mockito.exceptions.misusing.MissingMethodInvocationException;
+import org.mockito.quality.Strictness;
+
+import static android.provider.Settings.Global.DEVICE_NAME;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.clearInvocations;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockingDetails;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.reset;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.staticMockMarker;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verifyZeroInteractions;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+
+public class MockStatic {
+ private static class SuperClass {
+ final String returnA() {
+ return "superA";
+ }
+
+ static String returnB() {
+ return "superB";
+ }
+
+ static String returnC() {
+ return "superC";
+ }
+ }
+
+ private static final class SubClass extends SuperClass {
+ static String recorded = null;
+
+ static String returnC() {
+ return "subC";
+ }
+
+ static final String record(String toRecord) {
+ recorded = toRecord;
+ return "record";
+ }
+ }
+
+ @Test
+ public void spyStatic() throws Exception {
+ ContentResolver resolver = InstrumentationRegistry.getTargetContext().getContentResolver();
+ String deviceName = Settings.Global.getString(resolver, DEVICE_NAME);
+
+ MockitoSession session = mockitoSession().spyStatic(Settings.Global.class).startMocking();
+ try {
+ // Cannot call when(Settings.getString(any(ContentResolver.class), eq("...")))
+ // as any(ContentResolver.class) returns null which makes getString fail. Hence need to
+ // use less lambda API
+ doReturn("23").when(() -> Settings.Global.getString(any
+ (ContentResolver.class), eq("twenty three")));
+
+ doReturn(42).when(() -> Settings.Global.getInt(any
+ (ContentResolver.class), eq("fourty two")));
+
+ // Make sure behavior is changed
+ assertEquals("23", Settings.Global.getString(resolver, "twenty three"));
+ assertEquals(42, Settings.Global.getInt(resolver, "fourty two"));
+
+ // Make sure non-mocked methods work as before
+ assertEquals(deviceName, Settings.Global.getString(resolver, DEVICE_NAME));
+ } finally {
+ session.finishMocking();
+ }
+ }
+
+ @Test
+ public void mockStatic() throws Exception {
+ ContentResolver resolver = InstrumentationRegistry.getTargetContext().getContentResolver();
+ String deviceName = Settings.Global.getString(resolver, DEVICE_NAME);
+
+ MockitoSession session = mockitoSession().mockStatic(Settings.Global.class).startMocking();
+ try {
+ // By default all static methods of the mocked class should return null/0/false
+ assertNull(Settings.Global.getString(resolver, DEVICE_NAME));
+
+ when(Settings.Global.getString(any(ContentResolver.class), eq(DEVICE_NAME)))
+ .thenReturn("This is a test");
+
+ // Make sure behavior is changed
+ assertEquals("This is a test", Settings.Global.getString(resolver, DEVICE_NAME));
+ } finally {
+ session.finishMocking();
+ }
+
+ // Once the mocking is removed, the behavior should be back to normal
+ assertEquals(deviceName, Settings.Global.getString(resolver, DEVICE_NAME));
+ }
+
+ @Test
+ public void mockOverriddenStaticMethod() throws Exception {
+ MockitoSession session = mockitoSession().mockStatic(SubClass.class).startMocking();
+ try {
+ // By default all static methods of the mocked class should return the default answers
+ assertNull(SubClass.returnB());
+ assertNull(SubClass.returnC());
+
+ // Super class is not mocked
+ assertEquals("superB", SuperClass.returnB());
+ assertEquals("superC", SuperClass.returnC());
+
+ when(SubClass.returnB()).thenReturn("fakeB");
+ when(SubClass.returnC()).thenReturn("fakeC");
+
+ // Make sure behavior is changed
+ assertEquals("fakeB", SubClass.returnB());
+ assertEquals("fakeC", SubClass.returnC());
+
+ // Super class should not be affected
+ assertEquals("superB", SuperClass.returnB());
+ assertEquals("superC", SuperClass.returnC());
+ } finally {
+ session.finishMocking();
+ }
+
+ // Mocking should be stopped
+ assertEquals("superB", SubClass.returnB());
+ assertEquals("subC", SubClass.returnC());
+ }
+
+ @Test
+ public void mockSuperMethod() throws Exception {
+ MockitoSession session = mockitoSession().mockStatic(SuperClass.class).startMocking();
+ try {
+ // By default all static methods of the mocked class should return the default answers
+ assertNull(SuperClass.returnB());
+ assertNull(SuperClass.returnC());
+
+ // Sub class should not be affected
+ assertEquals("superB", SubClass.returnB());
+ assertEquals("subC", SubClass.returnC());
+
+ when(SuperClass.returnB()).thenReturn("fakeB");
+ when(SuperClass.returnC()).thenReturn("fakeC");
+
+ // Make sure behavior is changed
+ assertEquals("fakeB", SuperClass.returnB());
+ assertEquals("fakeC", SuperClass.returnC());
+
+ // Sub class should not be affected
+ assertEquals("superB", SubClass.returnB());
+ assertEquals("subC", SubClass.returnC());
+ } finally {
+ session.finishMocking();
+ }
+
+ // Mocking should be stopped
+ assertEquals("superB", SuperClass.returnB());
+ assertEquals("superC", SuperClass.returnC());
+ }
+
+ @Test(expected = MissingMethodInvocationException.class)
+ public void nonMockedTest() throws Exception {
+ when(SuperClass.returnB()).thenReturn("fakeB");
+ }
+
+ @Test
+ public void resetMock() throws Exception {
+ MockitoSession session = mockitoSession().mockStatic(SuperClass.class).startMocking();
+ try {
+ assertNull(SuperClass.returnB());
+
+ when(SuperClass.returnB()).thenReturn("fakeB");
+ assertEquals("fakeB", SuperClass.returnB());
+
+ reset(staticMockMarker(SuperClass.class));
+ assertNull(SuperClass.returnB());
+ } finally {
+ session.finishMocking();
+ }
+ }
+
+ @Test
+ public void resetSpy() throws Exception {
+ MockitoSession session = mockitoSession().spyStatic(SuperClass.class).startMocking();
+ try {
+ assertEquals("superB", SuperClass.returnB());
+
+ when(SuperClass.returnB()).thenReturn("fakeB");
+ assertEquals("fakeB", SuperClass.returnB());
+
+ reset(staticMockMarker(SuperClass.class));
+ assertEquals("superB", SuperClass.returnB());
+ } finally {
+ session.finishMocking();
+ }
+ }
+
+ @Test
+ public void staticMockingIsSeparateFromNonStaticMocking() throws Exception {
+ SuperClass objA = new SuperClass();
+ SuperClass objB;
+
+ MockitoSession session = mockitoSession().mockStatic(SuperClass.class).startMocking();
+ try {
+ assertNull(SuperClass.returnB());
+ assertNull(objA.returnB());
+
+ objB = mock(SuperClass.class);
+
+ assertEquals("superA", objA.returnA());
+
+ // Any kind of static method method call should be mocked
+ assertNull(objB.returnA());
+
+ assertNull(SuperClass.returnB());
+ assertNull(objA.returnB());
+ assertNull(objB.returnB());
+ } finally {
+ session.finishMocking();
+ }
+
+ assertEquals("superA", objA.returnA());
+ assertNull(objB.returnA());
+
+ // Any kind of static method method call should _not_ be mocked
+ assertEquals("superB", SuperClass.returnB());
+ assertEquals("superB", objA.returnB());
+ assertEquals("superB", objB.returnB());
+ }
+
+ @Test
+ public void mockWithTwoClasses() throws Exception {
+ MockitoSession session = mockitoSession().mockStatic(SuperClass.class)
+ .mockStatic(SubClass.class).startMocking();
+ try {
+ when(SuperClass.returnB()).thenReturn("fakeB");
+ assertEquals("fakeB", SuperClass.returnB());
+
+ when(SubClass.returnC()).thenReturn("fakeC");
+ assertEquals("fakeC", SubClass.returnC());
+ } finally {
+ session.finishMocking();
+ }
+ }
+
+ @Test
+ public void doReturnMockWithTwoClasses() throws Exception {
+ MockitoSession session = mockitoSession().mockStatic(SuperClass.class)
+ .mockStatic(SubClass.class).startMocking();
+ try {
+ doReturn("fakeB").when(SuperClass::returnB);
+ assertEquals("fakeB", SuperClass.returnB());
+
+ doReturn("fakeD").when(() -> SubClass.record("test"));
+ assertEquals("fakeD", SubClass.record("test"));
+ } finally {
+ session.finishMocking();
+ }
+ }
+
+ @Test
+ public void doReturnTwice() throws Exception {
+ MockitoSession session = mockitoSession().mockStatic(SuperClass.class).startMocking();
+ try {
+ doReturn("fakeB").doReturn("fakeB2").when(SuperClass::returnB);
+ assertEquals("fakeB", SuperClass.returnB());
+ assertEquals("fakeB2", SuperClass.returnB());
+ } finally {
+ session.finishMocking();
+ }
+ }
+
+ @Test
+ public void doReturnSpyHasNoSideEffect() throws Exception {
+ MockitoSession session = mockitoSession().spyStatic(SubClass.class).startMocking();
+ try {
+ SubClass.recorded = null;
+ SubClass.record("no sideeffect");
+ assertEquals("no sideeffect", SubClass.recorded);
+
+ doReturn("faceRecord").when(() -> SubClass.record(eq("test")));
+ // Verify that there was no side effect as the lambda gets intercepted
+ assertEquals("no sideeffect", SubClass.recorded);
+
+ assertEquals("faceRecord", SubClass.record("test"));
+ // Verify that there was no side effect as the method is stubbed
+ assertEquals("no sideeffect", SubClass.recorded);
+ } finally {
+ session.finishMocking();
+ }
+ }
+
+ @Test
+ public void onlyOneMethodCallDuringStubbing() throws Exception {
+ MockitoSession session = mockitoSession().strictness(Strictness.LENIENT)
+ .spyStatic(SuperClass.class).startMocking();
+ try {
+ try {
+ doReturn("").when(() -> {
+ SuperClass.returnB();
+ SuperClass.returnC();
+ });
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertTrue(e.getMessage(), e.getMessage().contains("returnB"));
+ assertTrue(e.getMessage(), e.getMessage().contains("returnC"));
+
+ assertFalse(e.getMessage(), e.getMessage().contains("returnA"));
+ }
+ } finally {
+ session.finishMocking();
+ }
+ }
+
+ @Test
+ public void atLeastOneMethodCallDuringStubbing() throws Exception {
+ Exception atLeastOneMethodCallException = null;
+
+ try {
+ MockitoSession session = mockitoSession().spyStatic(SuperClass.class).startMocking();
+ try {
+ try {
+ doReturn("").when(() -> {
+ });
+ fail();
+ } catch (IllegalArgumentException expected) {
+ atLeastOneMethodCallException = expected;
+ }
+ } finally {
+ session.finishMocking();
+ }
+ } catch (Throwable ignored) {
+ // We don't want to test exceptions form MockitoSession
+ }
+
+ assertNotNull(atLeastOneMethodCallException);
+ }
+
+ @Test
+ public void clearInvocationsRemovedInvocations() throws Exception {
+ MockitoSession session = mockitoSession().mockStatic(SuperClass.class).startMocking();
+ try {
+ SuperClass.returnB();
+ clearInvocations(staticMockMarker(SuperClass.class));
+ verifyZeroInteractions(staticMockMarker(SuperClass.class));
+ } finally {
+ session.finishMocking();
+ }
+ }
+
+ @Test
+ public void verifyMockingDetails() throws Exception {
+ MockitoSession session = mockitoSession().mockStatic(SuperClass.class)
+ .spyStatic(SubClass.class).startMocking();
+ try {
+ when(SuperClass.returnB()).thenReturn("fakeB");
+ SuperClass.returnB();
+ SuperClass.returnC();
+
+ MockingDetails superClassDetails = mockingDetails(staticMockMarker(SuperClass.class));
+ assertTrue(superClassDetails.isMock());
+ assertFalse(superClassDetails.isSpy());
+ assertEquals(2, superClassDetails.getInvocations().size());
+ assertEquals(1, superClassDetails.getStubbings().size());
+
+ MockingDetails subClassDetails = mockingDetails(staticMockMarker(SubClass.class));
+ assertTrue(subClassDetails.isMock());
+ assertTrue(subClassDetails.isSpy());
+ } finally {
+ session.finishMocking();
+ }
+ }
+}
diff --git a/dexmaker-mockito-inline-extended-tests/src/main/java/com/android/dx/mockito/inline/extended/tests/SpyOn.java b/dexmaker-mockito-inline-extended-tests/src/main/java/com/android/dx/mockito/inline/extended/tests/SpyOn.java
new file mode 100644
index 0000000..b116498
--- /dev/null
+++ b/dexmaker-mockito-inline-extended-tests/src/main/java/com/android/dx/mockito/inline/extended/tests/SpyOn.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2018 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.extended.tests;
+
+import android.app.Instrumentation;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertSame;
+
+@RunWith(AndroidJUnit4.class)
+public class SpyOn {
+ @Rule
+ public ActivityTestRule<EmptyActivity> activityRule =
+ new ActivityTestRule<>(EmptyActivity.class);
+
+ public class TestClass {
+ Object field;
+
+ public String echo(String in) {
+ return in;
+ }
+ }
+
+ @Test
+ public void spyOnLocalClass() {
+ TestClass t = new TestClass();
+ assertEquals("one", t.echo("one"));
+
+ spyOn(t);
+ assertEquals("two", t.echo("two"));
+ verify(t).echo("two");
+
+ when(t.echo("three")).thenReturn("not three");
+ assertEquals("not three", t.echo("three"));
+ verify(t).echo("three");
+ }
+
+ @Test
+ public void localFieldStaysTheSame() {
+ TestClass t = new TestClass();
+
+ Object marker = mock(Object.class);
+ t.field = marker;
+
+ spyOn(t);
+ assertSame(marker, t.field);
+ }
+
+ @Test
+ public void spiesAreUsuallyClones() {
+ TestClass original = new TestClass();
+
+ Object marker = new Object();
+ original.field = marker;
+
+ TestClass spy = spy(original);
+ assertSame(marker, spy.field);
+
+ assertNotSame(original, spy);
+ }
+
+ @Test
+ public void spyOnActivity() throws Exception {
+ EmptyActivity a = activityRule.getActivity();
+ spyOn(a);
+
+ // Intercept a#onDestroy(). The first time this is called isDestroyed[0] is set to true,
+ // the second time it is called, it calls the real method.
+ boolean isDestroyed[] = new boolean[]{false};
+ doAnswer((inv) -> {
+ synchronized (isDestroyed) {
+ isDestroyed[0] = true;
+ isDestroyed.notifyAll();
+ }
+
+ // Call a second time to call super method before returning. Android requires onDestroy
+ // to always call it's super-method.
+ a.onDestroy();
+ return null;
+ }).doCallRealMethod().when(a).onDestroy();
+
+ activityRule.finishActivity();
+
+ synchronized (isDestroyed) {
+ while (!isDestroyed[0]) {
+ isDestroyed.wait();
+ }
+ }
+ }
+}
diff --git a/dexmaker-mockito-inline-extended-tests/src/main/java/com/android/dx/mockito/inline/extended/tests/StaticMockitoSession.java b/dexmaker-mockito-inline-extended-tests/src/main/java/com/android/dx/mockito/inline/extended/tests/StaticMockitoSession.java
new file mode 100644
index 0000000..7c7941b
--- /dev/null
+++ b/dexmaker-mockito-inline-extended-tests/src/main/java/com/android/dx/mockito/inline/extended/tests/StaticMockitoSession.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2018 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.extended.tests;
+
+import android.content.ContentResolver;
+import android.provider.Settings;
+
+import org.junit.Test;
+import org.mockito.MockitoSession;
+import org.mockito.exceptions.misusing.UnnecessaryStubbingException;
+import org.mockito.quality.Strictness;
+
+import static android.provider.Settings.Global.DEVICE_NAME;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+
+public class StaticMockitoSession {
+ @Test
+ public void strictUnnecessaryStubbing() throws Exception {
+ MockitoSession session = mockitoSession().spyStatic(Settings.Global.class).startMocking();
+
+ // Set up unnecessary stubbing
+ doReturn("23").when(() -> Settings.Global.getString(any
+ (ContentResolver.class), eq(DEVICE_NAME)));
+
+ try {
+ session.finishMocking();
+ fail();
+ } catch (UnnecessaryStubbingException e) {
+ assertTrue("\"" + e.getMessage() + "\" does not contain 'Settings$Global.getString'",
+ e.getMessage().contains("Settings$Global.getString"));
+ }
+ }
+
+ @Test
+ public void lenientUnnecessaryStubbing() throws Exception {
+ MockitoSession session = mockitoSession().strictness(Strictness.LENIENT)
+ .spyStatic(Settings.Global.class).startMocking();
+
+ // Set up unnecessary stubbing
+ doReturn("23").when(() -> Settings.Global.getString(any
+ (ContentResolver.class), eq(DEVICE_NAME)));
+
+ session.finishMocking();
+ }
+}
diff --git a/dexmaker-mockito-inline-extended-tests/src/main/java/com/android/dx/mockito/inline/extended/tests/StaticMockitoSessionVsMockitoJUnitRunner.java b/dexmaker-mockito-inline-extended-tests/src/main/java/com/android/dx/mockito/inline/extended/tests/StaticMockitoSessionVsMockitoJUnitRunner.java
new file mode 100644
index 0000000..fe75755
--- /dev/null
+++ b/dexmaker-mockito-inline-extended-tests/src/main/java/com/android/dx/mockito/inline/extended/tests/StaticMockitoSessionVsMockitoJUnitRunner.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2018 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.extended.tests;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class StaticMockitoSessionVsMockitoJUnitRunner {
+ @Test
+ public void simpleStubbing() throws Exception {
+ (new MockStatic()).spyStatic();
+ }
+}
diff --git a/dexmaker-mockito-inline-extended-tests/src/main/java/com/android/dx/mockito/inline/extended/tests/Stress.java b/dexmaker-mockito-inline-extended-tests/src/main/java/com/android/dx/mockito/inline/extended/tests/Stress.java
new file mode 100644
index 0000000..6d449bb
--- /dev/null
+++ b/dexmaker-mockito-inline-extended-tests/src/main/java/com/android/dx/mockito/inline/extended/tests/Stress.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2018 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.extended.tests;
+
+import android.util.Log;
+
+import org.junit.Test;
+import org.mockito.MockitoSession;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.staticMockMarker;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.reset;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.mockito.Mockito.when;
+
+
+public class Stress {
+ private static final String LOG_TAG = Stress.class.getSimpleName();
+
+ private static class SuperClass {
+ static String returnB() {
+ return "superB";
+ }
+
+ final static String returnD() {
+ return "superD";
+ }
+ }
+
+ @Test
+ public void stressFinalStaticMethod() throws Exception {
+ MockitoSession session = mockitoSession().mockStatic(SuperClass.class).startMocking();
+ try {
+ assertNull(SuperClass.returnD());
+
+ for (int i = 0; i < 1000; i++) {
+ when(SuperClass.returnD()).thenReturn("fakeD");
+ assertEquals("fakeD", SuperClass.returnD());
+
+ reset(staticMockMarker(SuperClass.class));
+ assertNull(SuperClass.returnD());
+
+ if (i % 100 == 0) {
+ Log.i(LOG_TAG, "Ran " + i + " tests");
+ }
+ }
+ } finally {
+ session.finishMocking();
+ }
+ }
+
+ @Test
+ public void stressStaticMethod() throws Exception {
+ MockitoSession session = mockitoSession().mockStatic(SuperClass.class).startMocking();
+ try {
+ assertNull(SuperClass.returnB());
+
+ for (int i = 0; i < 10; i++) {
+ when(SuperClass.returnB()).thenReturn("fakeB");
+ assertEquals("fakeB", SuperClass.returnB());
+
+ reset(staticMockMarker(SuperClass.class));
+ assertNull(SuperClass.returnB());
+
+ Log.i(LOG_TAG, "Ran " + i + " tests");
+ }
+ } finally {
+ session.finishMocking();
+ }
+ }
+
+}
diff --git a/dexmaker-mockito-inline-extended-tests/src/main/java/com/android/dx/mockito/inline/extended/tests/VerifyStatic.java b/dexmaker-mockito-inline-extended-tests/src/main/java/com/android/dx/mockito/inline/extended/tests/VerifyStatic.java
new file mode 100644
index 0000000..afb1439
--- /dev/null
+++ b/dexmaker-mockito-inline-extended-tests/src/main/java/com/android/dx/mockito/inline/extended/tests/VerifyStatic.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2018 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.extended.tests;
+
+import com.android.dx.mockito.inline.extended.StaticInOrder;
+
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.MockitoSession;
+import org.mockito.exceptions.verification.NoInteractionsWanted;
+import org.mockito.exceptions.verification.VerificationInOrderFailure;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.ignoreStubs;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.inOrder;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.staticMockMarker;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verifyNoMoreInteractions;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verifyZeroInteractions;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.eq;
+
+public class VerifyStatic {
+ @Test
+ public void verifyMockedStringMethod() throws Exception {
+ MockitoSession session = mockitoSession().mockStatic(EchoClass.class).startMocking();
+ try {
+ assertNull(EchoClass.echo("marco!"));
+
+ ArgumentCaptor<String> echoCaptor = ArgumentCaptor.forClass(String.class);
+ verify(() -> {return EchoClass.echo(echoCaptor.capture());});
+ assertEquals("marco!", echoCaptor.getValue());
+
+ verifyNoMoreInteractions(staticMockMarker(EchoClass.class));
+ } finally {
+ session.finishMocking();
+ }
+ }
+
+ @Test
+ public void verifyMockedVoidMethod() throws Exception {
+ MockitoSession session = mockitoSession().mockStatic(ConsumeClass.class).startMocking();
+ try {
+ ConsumeClass.consume("donut");
+
+ ArgumentCaptor<String> yumCaptor = ArgumentCaptor.forClass(String.class);
+ verify(() -> ConsumeClass.consume(yumCaptor.capture()));
+
+ verifyNoMoreInteractions(staticMockMarker(ConsumeClass.class));
+ } finally {
+ session.finishMocking();
+ }
+ }
+
+ @Test
+ public void verifyWithTwoMocks() throws Exception {
+ MockitoSession session = mockitoSession().mockStatic(EchoClass.class)
+ .mockStatic(ConsumeClass.class).startMocking();
+ try {
+ ConsumeClass.consume("donut");
+ assertNull(EchoClass.echo("marco!"));
+
+ ArgumentCaptor<String> yumCaptor = ArgumentCaptor.forClass(String.class);
+ verify(() -> ConsumeClass.consume(yumCaptor.capture()));
+
+ ArgumentCaptor<String> echoCaptor = ArgumentCaptor.forClass(String.class);
+ verify(() -> {return EchoClass.echo(echoCaptor.capture());});
+ assertEquals("marco!", echoCaptor.getValue());
+
+ verifyNoMoreInteractions(staticMockMarker(ConsumeClass.class));
+ } finally {
+ session.finishMocking();
+ }
+ }
+
+ @Test
+ public void verifySpiedStringMethod() throws Exception {
+ MockitoSession session = mockitoSession().spyStatic(EchoClass.class).startMocking();
+ try {
+ assertEquals("marco!", EchoClass.echo("marco!"));
+
+ ArgumentCaptor<String> echoCaptor = ArgumentCaptor.forClass(String.class);
+ verify(() -> {return EchoClass.echo(echoCaptor.capture());});
+ assertEquals("marco!", echoCaptor.getValue());
+
+ verifyNoMoreInteractions(staticMockMarker(EchoClass.class));
+ } finally {
+ session.finishMocking();
+ }
+ }
+
+ @Test
+ public void verifyInOrder() throws Exception {
+ MockitoSession session = mockitoSession().mockStatic(EchoClass.class).mockStatic
+ (ConsumeClass.class).startMocking();
+ try {
+ EchoClass.echo("marco!");
+ ConsumeClass.consume("donuts");
+ ConsumeClass.consume("nougat");
+ EchoClass.echo("polo");
+
+ StaticInOrder echoInOrder = inOrder(staticMockMarker(EchoClass.class));
+ echoInOrder.verify(() -> EchoClass.echo(eq("marco!")));
+ echoInOrder.verify(() -> EchoClass.echo(eq("polo")));
+ echoInOrder.verifyNoMoreInteractions();
+
+ StaticInOrder consumeInOrder = inOrder(staticMockMarker(ConsumeClass.class));
+ consumeInOrder.verify(() -> ConsumeClass.consume(eq("donuts")));
+ consumeInOrder.verify(() -> ConsumeClass.consume(eq("nougat")));
+ consumeInOrder.verifyNoMoreInteractions();
+
+ StaticInOrder combinedInOrder = inOrder(staticMockMarker(EchoClass.class,
+ ConsumeClass.class));
+ combinedInOrder.verify(() -> EchoClass.echo(eq("marco!")));
+ combinedInOrder.verify(() -> ConsumeClass.consume(eq("donuts")));
+ combinedInOrder.verify(() -> ConsumeClass.consume(eq("nougat")));
+ combinedInOrder.verify(() -> EchoClass.echo(eq("polo")));
+ combinedInOrder.verifyNoMoreInteractions();
+ } finally {
+ session.finishMocking();
+ }
+ }
+
+ @Test(expected = VerificationInOrderFailure.class)
+ public void verifyBadOrder() throws Exception {
+ MockitoSession session = mockitoSession().mockStatic(EchoClass.class).startMocking();
+ try {
+ EchoClass.echo("marco!");
+ EchoClass.echo("polo");
+
+ StaticInOrder echoInOrder = inOrder(staticMockMarker(EchoClass.class));
+ echoInOrder.verify(() -> EchoClass.echo(eq("polo")));
+ echoInOrder.verify(() -> EchoClass.echo(eq("marco!")));
+ } finally {
+ session.finishMocking();
+ }
+ }
+
+ @Test
+ public void verifyBadMatcher() throws Exception {
+ MockitoSession session = mockitoSession().mockStatic(EchoClass.class).startMocking();
+ try {
+ EchoClass.echo("marco!");
+ EchoClass.echo("polo");
+
+ StaticInOrder echoInOrder = inOrder(staticMockMarker(EchoClass.class));
+ echoInOrder.verify(() -> EchoClass.echo(eq("marco!")));
+
+ try {
+ echoInOrder.verify(() -> EchoClass.echo(eq("badMarker")));
+ fail();
+ } catch (VerificationInOrderFailure e) {
+ assertTrue(e.getMessage(), e.getMessage().contains("badMarker"));
+ }
+ } finally {
+ session.finishMocking();
+ }
+ }
+
+ @Test(expected = NoInteractionsWanted.class)
+ public void zeroInvocationsThrowsIfThereWasAnInvocation() throws Exception {
+ MockitoSession session = mockitoSession().mockStatic(EchoClass.class).startMocking();
+ try {
+ EchoClass.echo("marco!");
+ verifyZeroInteractions(staticMockMarker(EchoClass.class));
+ fail();
+ } finally {
+ session.finishMocking();
+ }
+ }
+
+ @Test
+ public void verifyWithIgnoreStubs() throws Exception {
+ MockitoSession session = mockitoSession().spyStatic(EchoClass.class).startMocking();
+ try {
+ // 'ignoreStubs' only ignore stubs
+ when(EchoClass.echo("marco!")).thenReturn("polo");
+ assertEquals("polo", EchoClass.echo("marco!"));
+ assertEquals("echo", EchoClass.echo("echo"));
+
+ verify(() -> {return EchoClass.echo(eq("echo"));});
+ verifyNoMoreInteractions(ignoreStubs(staticMockMarker(EchoClass.class)));
+ } finally {
+ session.finishMocking();
+ }
+ }
+
+ private static class EchoClass {
+ static final String echo(String echo) {
+ return echo;
+ }
+ }
+
+ private static class ConsumeClass {
+ static final void consume(String yum) {
+ // empty
+ }
+ }
+}
diff --git a/dexmaker-mockito-inline-extended/CMakeLists.txt b/dexmaker-mockito-inline-extended/CMakeLists.txt
new file mode 100644
index 0000000..cec4ec0
--- /dev/null
+++ b/dexmaker-mockito-inline-extended/CMakeLists.txt
@@ -0,0 +1,33 @@
+cmake_minimum_required(VERSION 3.4.1)
+
+set(slicer_sources
+ ../dexmaker-mockito-inline/external/slicer/bytecode_encoder.cc
+ ../dexmaker-mockito-inline/external/slicer/code_ir.cc
+ ../dexmaker-mockito-inline/external/slicer/common.cc
+ ../dexmaker-mockito-inline/external/slicer/control_flow_graph.cc
+ ../dexmaker-mockito-inline/external/slicer/debuginfo_encoder.cc
+ ../dexmaker-mockito-inline/external/slicer/dex_bytecode.cc
+ ../dexmaker-mockito-inline/external/slicer/dex_format.cc
+ ../dexmaker-mockito-inline/external/slicer/dex_ir_builder.cc
+ ../dexmaker-mockito-inline/external/slicer/dex_ir.cc
+ ../dexmaker-mockito-inline/external/slicer/dex_utf8.cc
+ ../dexmaker-mockito-inline/external/slicer/instrumentation.cc
+ ../dexmaker-mockito-inline/external/slicer/reader.cc
+ ../dexmaker-mockito-inline/external/slicer/tryblocks_encoder.cc
+ ../dexmaker-mockito-inline/external/slicer/writer.cc)
+
+add_library(slicer
+ STATIC
+ ${slicer_sources})
+
+include_directories(../dexmaker-mockito-inline/external/jdk ../dexmaker-mockito-inline/external/slicer/export/)
+
+target_link_libraries(slicer z)
+
+add_library(staticjvmtiagent
+ SHARED
+ src/main/jni/staticjvmtiagent/agent.cc)
+
+set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DANDROID_STL=c++_shared -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(staticjvmtiagent slicer)
diff --git a/dexmaker-mockito-inline-extended/build.gradle b/dexmaker-mockito-inline-extended/build.gradle
new file mode 100644
index 0000000..055168b
--- /dev/null
+++ b/dexmaker-mockito-inline-extended/build.gradle
@@ -0,0 +1,123 @@
+buildscript {
+ repositories {
+ maven {
+ url "https://plugins.gradle.org/m2/"
+ }
+ }
+ dependencies {
+ classpath "net.ltgt.gradle:gradle-errorprone-plugin:0.0.13"
+ }
+}
+
+apply plugin: "net.ltgt.errorprone"
+apply plugin: 'com.android.library'
+apply plugin: 'maven-publish'
+apply plugin: 'ivy-publish'
+apply plugin: 'com.jfrog.artifactory'
+
+version = VERSION_NAME
+
+android {
+ compileSdkVersion 28
+ buildToolsVersion '28.0.0'
+
+ android {
+ lintOptions {
+ disable 'InvalidPackage'
+ warning 'NewApi'
+ }
+ }
+
+ defaultConfig {
+ minSdkVersion 1
+ targetSdkVersion 28
+ versionName VERSION_NAME
+ }
+
+ externalNativeBuild {
+ cmake {
+ path 'CMakeLists.txt'
+ }
+ }
+
+ compileOptions {
+ targetCompatibility 1.8
+ sourceCompatibility 1.8
+ }
+}
+
+tasks.withType(JavaCompile) {
+ options.compilerArgs += ["-Xep:StringSplitter:OFF"]
+}
+
+task sourcesJar(type: Jar) {
+ classifier = 'sources'
+ from android.sourceSets.main.java.srcDirs
+}
+
+task javadoc(type: Javadoc) {
+ source = android.sourceSets.main.java.srcDirs
+ classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
+ failOnError false
+}
+
+task javadocJar(type: Jar, dependsOn: javadoc) {
+ classifier = 'javadoc'
+ from javadoc.destinationDir
+}
+
+publishing {
+ publications {
+ ivyLib(IvyPublication) {
+ from new org.gradle.api.internal.java.JavaLibrary(new org.gradle.api.internal.artifacts.publish.DefaultPublishArtifact(project.getName(), 'aar', 'aar', null, new Date(), new File("$buildDir/outputs/aar/${project.getName()}-release.aar"), assemble), project.configurations.implementation.getAllDependencies())
+ artifact sourcesJar
+ artifact javadocJar
+ }
+
+ lib(MavenPublication) {
+ from new org.gradle.api.internal.java.JavaLibrary(new org.gradle.api.internal.artifacts.publish.DefaultPublishArtifact(project.getName(), 'aar', 'aar', null, new Date(), new File("$buildDir/outputs/aar/${project.getName()}-release.aar"), assemble), project.configurations.implementation.getAllDependencies())
+
+ artifact sourcesJar
+ artifact javadocJar
+
+ pom.withXml {
+ asNode().children().last() + {
+ resolveStrategy = Closure.DELEGATE_FIRST
+ description = 'Extension of the Mockito Inline API to allow mocking static methods on the Android Dalvik VM'
+ url 'https://github.com/linkedin/dexmaker'
+ scm {
+ url 'https://github.com/linkedin/dexmaker'
+ connection 'scm:git:git://github.com/linkedin/dexmaker.git'
+ developerConnection 'https://github.com/linkedin/dexmaker.git'
+ }
+ licenses {
+ license {
+ name 'The Apache Software License, Version 2.0'
+ url 'http://www.apache.org/license/LICENSE-2.0.txt'
+ distribution 'repo'
+ }
+ }
+
+ developers {
+ developer {
+ id 'com.linkedin'
+ name 'LinkedIn Corp'
+ email ''
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+repositories {
+ jcenter()
+ google()
+}
+
+dependencies {
+ implementation project(':dexmaker-mockito-inline')
+
+ implementation 'org.mockito:mockito-core:2.19.0', { exclude group: 'net.bytebuddy' }
+}
diff --git a/dexmaker-mockito-inline-extended/src/main/AndroidManifest.xml b/dexmaker-mockito-inline-extended/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..a521602
--- /dev/null
+++ b/dexmaker-mockito-inline-extended/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.dx.mockito.inline.extended" />
diff --git a/dexmaker-mockito-inline-extended/src/main/java/com/android/dx/mockito/inline/InlineStaticMockMaker.java b/dexmaker-mockito-inline-extended/src/main/java/com/android/dx/mockito/inline/InlineStaticMockMaker.java
new file mode 100644
index 0000000..cf6a95b
--- /dev/null
+++ b/dexmaker-mockito-inline-extended/src/main/java/com/android/dx/mockito/inline/InlineStaticMockMaker.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2018 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 org.mockito.Mockito;
+import org.mockito.creation.instance.Instantiator;
+import org.mockito.exceptions.base.MockitoException;
+import org.mockito.invocation.MockHandler;
+import org.mockito.mock.MockCreationSettings;
+import org.mockito.plugins.InstantiatorProvider2;
+import org.mockito.plugins.MockMaker;
+
+import java.io.IOException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.BiConsumer;
+
+/**
+ * Creates mock markers and adds stubbing hooks to static method
+ *
+ * <p>This is done by transforming the byte code of the classes to add method entry hooks.
+ */
+public final class InlineStaticMockMaker implements MockMaker {
+ /**
+ * {@link StaticJvmtiAgent} set up during one time init
+ */
+ private static final StaticJvmtiAgent AGENT;
+
+ /**
+ * Error during one time init or {@code null} if init was successful
+ */
+ private static final Throwable INITIALIZATION_ERROR;
+ public static ThreadLocal<Class> mockingInProgressClass = new ThreadLocal<>();
+ public static ThreadLocal<BiConsumer<Class<?>, Method>> onMethodCallDuringStubbing
+ = new ThreadLocal<>();
+ public static ThreadLocal<BiConsumer<Class<?>, Method>> onMethodCallDuringVerification
+ = new ThreadLocal<>();
+
+ /*
+ * One time setup to allow the system to mocking via this mock maker.
+ */
+ static {
+ StaticJvmtiAgent agent;
+ Throwable initializationError = null;
+
+ try {
+ try {
+ agent = new StaticJvmtiAgent();
+ } 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\nPotentially, the current VM does not support the jvmti API " +
+ "correctly", ioe);
+ }
+ } catch (Throwable throwable) {
+ agent = null;
+ initializationError = throwable;
+ }
+
+ AGENT = agent;
+ INITIALIZATION_ERROR = initializationError;
+ }
+
+ /**
+ * All currently active mock markers. 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 HashMap<Object, InvocationHandlerAdapter> markerToHandler = new HashMap<>();
+ private final Map<Class, Object> classToMarker = new HashMap<>();
+
+ /**
+ * Class doing the actual byte code transformation.
+ */
+ private final StaticClassTransformer classTransformer;
+
+ /**
+ * Create a new mock maker.
+ */
+ public InlineStaticMockMaker() {
+ if (INITIALIZATION_ERROR != null) {
+ throw new RuntimeException("Could not initialize inline mock maker.\n" + "\n" +
+ "Release: Android " + Build.VERSION.RELEASE + " " + Build.VERSION.INCREMENTAL
+ + "Device: " + Build.BRAND + " " + Build.MODEL, INITIALIZATION_ERROR);
+ }
+
+ classTransformer = new StaticClassTransformer(AGENT, InlineDexmakerMockMaker
+ .DISPATCHER_CLASS, markerToHandler, classToMarker);
+ }
+
+ @Override
+ public <T> T createMock(MockCreationSettings<T> settings, MockHandler handler) {
+ Class<T> typeToMock = settings.getTypeToMock();
+ if (!typeToMock.equals(mockingInProgressClass.get()) || Modifier.isAbstract(typeToMock
+ .getModifiers())) {
+ return null;
+ }
+
+ Set<Class<?>> interfacesSet = settings.getExtraInterfaces();
+ InvocationHandlerAdapter handlerAdapter = new InvocationHandlerAdapter(handler);
+
+ classTransformer.mockClass(MockFeatures.withMockFeatures(typeToMock, interfacesSet));
+
+ Instantiator instantiator = Mockito.framework().getPlugins().getDefaultPlugin
+ (InstantiatorProvider2.class).getInstantiator(settings);
+
+ T mock;
+ try {
+ mock = instantiator.newInstance(typeToMock);
+ } catch (org.mockito.creation.instance.InstantiationException e) {
+ throw new MockitoException("Unable to create mock instance of type '" + typeToMock
+ .getSimpleName() + "'", e);
+ }
+
+ if (classToMarker.containsKey(typeToMock)) {
+ throw new MockitoException(typeToMock + " is already mocked");
+ }
+ classToMarker.put(typeToMock, mock);
+
+ markerToHandler.put(mock, handlerAdapter);
+ return mock;
+ }
+
+ @Override
+ public void resetMock(Object mock, MockHandler newHandler, MockCreationSettings settings) {
+ InvocationHandlerAdapter adapter = getInvocationHandlerAdapter(mock);
+ if (adapter != null) {
+ if (mockingInProgressClass.get() == mock.getClass()) {
+ markerToHandler.remove(mock);
+ classToMarker.remove(mock.getClass());
+ } else {
+ adapter.setHandler(newHandler);
+ }
+ }
+ }
+
+ @Override
+ public TypeMockability isTypeMockable(final Class<?> type) {
+ if (mockingInProgressClass.get() == type) {
+ return new TypeMockability() {
+ @Override
+ public boolean mockable() {
+ return !Modifier.isAbstract(type.getModifiers()) && !type.isPrimitive() && type
+ != String.class;
+ }
+
+ @Override
+ public String nonMockableReason() {
+ if (Modifier.isAbstract(type.getModifiers())) {
+ return "abstract type";
+ }
+
+ if (type.isPrimitive()) {
+ return "primitive type";
+ }
+
+ if (type == String.class) {
+ return "string";
+ }
+
+ return "not handled type";
+ }
+ };
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public MockHandler getHandler(Object mock) {
+ InvocationHandlerAdapter adapter = getInvocationHandlerAdapter(mock);
+ return adapter != null ? adapter.getHandler() : null;
+ }
+
+ /**
+ * Get the {@link InvocationHandlerAdapter} registered for a marker.
+ *
+ * @param marker marker of the class that might have mocking set up
+ * @return adapter for this class, or {@code null} if not mocked
+ */
+ private InvocationHandlerAdapter getInvocationHandlerAdapter(Object marker) {
+ if (marker == null) {
+ return null;
+ }
+
+ return markerToHandler.get(marker);
+ }
+}
diff --git a/dexmaker-mockito-inline-extended/src/main/java/com/android/dx/mockito/inline/StaticClassTransformer.java b/dexmaker-mockito-inline-extended/src/main/java/com/android/dx/mockito/inline/StaticClassTransformer.java
new file mode 100644
index 0000000..a5ce9c0
--- /dev/null
+++ b/dexmaker-mockito-inline-extended/src/main/java/com/android/dx/mockito/inline/StaticClassTransformer.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2018 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 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.Collections;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Adds entry hooks (that eventually call into
+ * {@link StaticMockMethodAdvice#handle(Object, Method, Object[])} to all static methods of the
+ * supplied classes.
+ * <p></p>Transforming a class to add entry hooks follow the following simple steps:
+ * <ol>
+ * <li>{@link #mockClass(MockFeatures)}</li>
+ * <li>{@link StaticJvmtiAgent#requestTransformClasses(Class[])}</li>
+ * <li>{@link StaticJvmtiAgent#nativeRetransformClasses(Class[])}</li>
+ * <li>agent.cc::Transform</li>
+ * <li>{@link StaticJvmtiAgent#runTransformers(ClassLoader, String, Class, ProtectionDomain,
+ * byte[])}</li>
+ * <li>{@link #transform(Class, byte[])}</li>
+ * <li>{@link #nativeRedefine(String, byte[])}</li>
+ * </ol>
+ */
+class StaticClassTransformer {
+ // 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));
+ /**
+ * 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();
+
+ /**
+ * Jvmti agent responsible for triggering transformations
+ */
+ private final StaticJvmtiAgent agent;
+
+ /**
+ * Types that have already be transformed
+ */
+ private final Set<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;
+
+ /**
+ * Create a new transformer.
+ */
+ StaticClassTransformer(StaticJvmtiAgent agent, Class dispatcherClass,
+ Map<Object, InvocationHandlerAdapter> markerToHandler, Map<Class, Object>
+ classToMarker) {
+ this.agent = agent;
+ mockedTypes = Collections.synchronizedSet(new HashSet<Class<?>>());
+ identifier = String.valueOf(System.identityHashCode(this));
+ StaticMockMethodAdvice advice = new StaticMockMethodAdvice(markerToHandler, classToMarker);
+
+ 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();
+ }
+ }
+ }
+
+ /**
+ * Check if the class should be transformed.
+ *
+ * @param classBeingRedefined The class that might need to transformed
+ * @return {@code true} iff the class needs to be transformed
+ */
+ boolean shouldTransform(Class<?> classBeingRedefined) {
+ return classBeingRedefined != null && mockedTypes.contains(classBeingRedefined);
+ }
+
+ private native byte[] nativeRedefine(String identifier, byte[] original);
+}
diff --git a/dexmaker-mockito-inline-extended/src/main/java/com/android/dx/mockito/inline/StaticJvmtiAgent.java b/dexmaker-mockito-inline-extended/src/main/java/com/android/dx/mockito/inline/StaticJvmtiAgent.java
new file mode 100644
index 0000000..63a4e49
--- /dev/null
+++ b/dexmaker-mockito-inline-extended/src/main/java/com/android/dx/mockito/inline/StaticJvmtiAgent.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2018 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 android.os.Debug;
+
+import java.io.IOException;
+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 StaticJvmtiAgent {
+ private static final String AGENT_LIB_NAME = "libstaticjvmtiagent.so";
+
+ private static final Object lock = new Object();
+
+ /**
+ * Registered byte code transformers
+ */
+ private final ArrayList<StaticClassTransformer> transformers = new ArrayList<>();
+
+ /**
+ * 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
+ */
+ StaticJvmtiAgent() throws IOException {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
+ throw new IOException("Requires API level " + Build.VERSION_CODES.P + ". API level is "
+ + Build.VERSION.SDK_INT);
+ }
+
+ ClassLoader cl = StaticJvmtiAgent.class.getClassLoader();
+ if (!(cl instanceof BaseDexClassLoader)) {
+ throw new IOException("Could not load jvmti plugin as StaticJvmtiAgent class was not loaded "
+ + "by a BaseDexClassLoader");
+ }
+
+ Debug.attachJvmtiAgent(AGENT_LIB_NAME, null, cl);
+ nativeRegisterTransformerHook();
+ }
+
+ private native void nativeRegisterTransformerHook();
+
+ private native void nativeUnregisterTransformerHook();
+
+ @Override
+ protected void finalize() throws Throwable {
+ nativeUnregisterTransformerHook();
+ }
+
+ /**
+ * 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(StaticClassTransformer)
+ * 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);
+ }
+ }
+ }
+
+ // called by JNI
+ @SuppressWarnings("unused")
+ public boolean shouldTransform(Class<?> classBeingRedefined) {
+ for (StaticClassTransformer transformer : transformers) {
+ if (transformer.shouldTransform(classBeingRedefined)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * 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(StaticClassTransformer 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 (StaticClassTransformer transformer : transformers) {
+ transformedByteCode = transformer.transform(classBeingRedefined, transformedByteCode);
+ }
+
+ return transformedByteCode;
+ }
+
+ private native void nativeRetransformClasses(Class<?>[] classes);
+}
diff --git a/dexmaker-mockito-inline-extended/src/main/java/com/android/dx/mockito/inline/StaticMockMethodAdvice.java b/dexmaker-mockito-inline-extended/src/main/java/com/android/dx/mockito/inline/StaticMockMethodAdvice.java
new file mode 100644
index 0000000..18c1bcc
--- /dev/null
+++ b/dexmaker-mockito-inline-extended/src/main/java/com/android/dx/mockito/inline/StaticMockMethodAdvice.java
@@ -0,0 +1,377 @@
+/*
+ * Copyright (c) 2018 Mockito contributors
+ * This program is made available under the terms of the MIT License.
+ */
+
+package com.android.dx.mockito.inline;
+
+import java.lang.ref.WeakReference;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.function.BiConsumer;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static com.android.dx.mockito.inline.InlineStaticMockMaker.onMethodCallDuringStubbing;
+import static com.android.dx.mockito.inline.InlineStaticMockMaker.onMethodCallDuringVerification;
+
+/**
+ * Backend for the method entry hooks. Checks if the hooks should cause an interception or should
+ * be ignored.
+ */
+class StaticMockMethodAdvice {
+ /**
+ * Pattern to decompose a instrumentedMethodWithTypeAndSignature
+ */
+ private final static Pattern methodPattern = Pattern.compile("(.*)#(.*)\\((.*)\\)");
+ private final Map<Object, InvocationHandlerAdapter> markersToHandler;
+ private final Map<Class, Object> classToMarker;
+ @SuppressWarnings("ThreadLocalUsage")
+ private final SelfCallInfo selfCallInfo = new SelfCallInfo();
+
+ StaticMockMethodAdvice(Map<Object, InvocationHandlerAdapter> markerToHandler, Map<Class, Object>
+ classToMarker) {
+ this.markersToHandler = markerToHandler;
+ this.classToMarker = classToMarker;
+ }
+
+ /**
+ * Try to invoke the method {@code origin}.
+ *
+ * @param origin method to invoke
+ * @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[] arguments)
+ throws Throwable {
+ try {
+ return origin.invoke(null, arguments);
+ } catch (InvocationTargetException exception) {
+ throw exception.getCause();
+ }
+ }
+
+ private static Class<?> classForTypeName(String name) throws ClassNotFoundException {
+ if (name.endsWith("[]")) {
+ return Class.forName("[L" + name.substring(0, name.length() - 2) + ";");
+ } else {
+ return Class.forName(name);
+ }
+ }
+
+ private static Class nameToType(String name) throws ClassNotFoundException {
+ switch (name) {
+ case "byte":
+ return Byte.TYPE;
+ case "short":
+ return Short.TYPE;
+ case "int":
+ return Integer.TYPE;
+ case "long":
+ return Long.TYPE;
+ case "char":
+ return Character.TYPE;
+ case "float":
+ return Float.TYPE;
+ case "double":
+ return Double.TYPE;
+ case "boolean":
+ return Boolean.TYPE;
+ case "byte[]":
+ return byte[].class;
+ case "short[]":
+ return short[].class;
+ case "int[]":
+ return int[].class;
+ case "long[]":
+ return long[].class;
+ case "char[]":
+ return char[].class;
+ case "float[]":
+ return float[].class;
+ case "double[]":
+ return double[].class;
+ case "boolean[]":
+ return boolean[].class;
+ default:
+ return classForTypeName(name);
+ }
+ }
+
+ /**
+ * Would a call to SubClass.method handled by SuperClass.method ?
+ * <p>This is the case when subclass or any intermediate parent does not override method.
+ *
+ * @param subclass Class that might have been called
+ * @param superClass Class defining the method
+ * @param methodName Name of method
+ * @param methodParameters Parameter of method
+ * @return {code true} iff the method would have be handled by superClass
+ */
+ private static boolean isMethodDefinedBySuperClass(Class<?> subclass, Class<?> superClass,
+ String methodName,
+ Class<?>[] methodParameters) {
+ do {
+ if (subclass == superClass) {
+ // The method is not overridden in the subclass or any class in between subClass
+ // and superClass.
+ return true;
+ }
+
+ try {
+ subclass.getDeclaredMethod(methodName, methodParameters);
+
+ // method is overridden is sub-class. hence the call could not have handled by
+ // the super-class.
+ return false;
+ } catch (NoSuchMethodException e) {
+ subclass = subclass.getSuperclass();
+ }
+ } while (subclass != null);
+
+ // Subclass is not a sub class of superClass
+ return false;
+ }
+
+ private static List<Class<?>> getAllSubclasses(Class<?> superClass, Collection<Class>
+ possibleSubClasses) {
+ ArrayList<Class<?>> subclasses = new ArrayList<>();
+ for (Class<?> possibleSubClass : possibleSubClasses) {
+ if (superClass.isAssignableFrom(possibleSubClass)) {
+ subclasses.add(possibleSubClass);
+ }
+ }
+
+ return subclasses;
+ }
+
+ private synchronized static native String nativeGetCalledClassName();
+
+ private Class<?> getClassMethodWasCalledOn(MethodDesc methodDesc) throws ClassNotFoundException,
+ NoSuchMethodException {
+ Class<?> classDeclaringMethod = classForTypeName(methodDesc.className);
+
+ /* If a sub-class does not override a static method, the super-classes method is called
+ * directly. Hence 'classDeclaringMethod' will be the super class. As the mocking of
+ * this and the class actually called might be different we need to find the class that
+ * was actually called.
+ */
+ if (Modifier.isFinal(classDeclaringMethod.getModifiers())
+ || Modifier.isFinal(classDeclaringMethod.getDeclaredMethod(methodDesc.methodName,
+ methodDesc.methodParamTypes).getModifiers())) {
+ return classDeclaringMethod;
+ } else {
+ boolean mightBeMocked = false;
+ // if neither the defining class nor any subclass of it is mocked, no point of
+ // trying to figure out the called class as isMocked will soon be checked.
+ for (Class<?> subClass : getAllSubclasses(classDeclaringMethod, classToMarker.keySet())) {
+ if (isMethodDefinedBySuperClass(subClass, classDeclaringMethod,
+ methodDesc.methodName, methodDesc.methodParamTypes)) {
+ mightBeMocked = true;
+ break;
+ }
+ }
+
+ if (!mightBeMocked) {
+ return null;
+ }
+
+ String calledClassName = nativeGetCalledClassName();
+ return Class.forName(calledClassName);
+ }
+ }
+
+ /**
+ * Get the method specified by {@code methodWithTypeAndSignature}.
+ *
+ * @param ignored
+ * @param methodWithTypeAndSignature the description of the method
+ * @return method {@code methodWithTypeAndSignature} refer to
+ */
+ @SuppressWarnings("unused")
+ public Method getOrigin(Object ignored, String methodWithTypeAndSignature) throws Throwable {
+ MethodDesc methodDesc = new MethodDesc(methodWithTypeAndSignature);
+
+ Class clazz = getClassMethodWasCalledOn(methodDesc);
+ if (clazz == null) {
+ return null;
+ }
+
+ Object marker = classToMarker.get(clazz);
+ if (!isMocked(marker)) {
+ return null;
+ }
+
+ return Class.forName(methodDesc.className).getDeclaredMethod(methodDesc.methodName,
+ methodDesc.methodParamTypes);
+ }
+
+ /**
+ * Handle a method entry hook.
+ *
+ * @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 methodDescStr, Method origin, Object[] arguments) throws
+ Throwable {
+ MethodDesc methodDesc = new MethodDesc((String) methodDescStr);
+ Class clazz = getClassMethodWasCalledOn(methodDesc);
+
+ Object marker = classToMarker.get(clazz);
+ InvocationHandlerAdapter interceptor = markersToHandler.get(marker);
+ if (interceptor == null) {
+ return null;
+ }
+
+ // extended.StaticCapableStubber#whenInt
+ BiConsumer<Class<?>, Method> onStub = onMethodCallDuringStubbing.get();
+ if (onStub != null) {
+ onStub.accept(clazz, origin);
+ }
+
+ // extended.ExtendedMockito#verifyInt
+ BiConsumer<Class<?>, Method> onVerify = onMethodCallDuringVerification.get();
+ if (onVerify != null) {
+ onVerify.accept(clazz, origin);
+ }
+
+ return new ReturnValueWrapper(interceptor.interceptEntryHook(marker, origin, arguments,
+ new SuperMethodCall(selfCallInfo, origin, marker, arguments)));
+ }
+
+ /**
+ * Checks if an {@code marker} is a mock marker.
+ *
+ * @return {@code true} iff the marker is a mock marker
+ */
+ public boolean isMarker(Object marker) {
+ return markersToHandler.containsKey(marker);
+ }
+
+ /**
+ * Check if this method call should be mocked. Usually the same as {@link #isMarker(Object)} but
+ * takes into account the state of {@link #selfCallInfo} that allows to temporary disable
+ * mocking for a single method call.
+ */
+ public boolean isMocked(Object marker) {
+ return selfCallInfo.shouldMockMethod(marker) && isMarker(marker);
+ }
+
+ private static class MethodDesc {
+ final String className;
+ final String methodName;
+ final Class<?>[] methodParamTypes;
+
+ private MethodDesc(String methodWithTypeAndSignature) throws ClassNotFoundException {
+ Matcher methodComponents = methodPattern.matcher(methodWithTypeAndSignature);
+ boolean wasFound = methodComponents.find();
+ if (!wasFound) {
+ throw new IllegalArgumentException();
+ }
+
+ className = methodComponents.group(1);
+ methodName = methodComponents.group(2);
+ String methodParamTypeNames[] = methodComponents.group(3).split(",");
+
+ ArrayList<Class<?>> methodParamTypesList = new ArrayList<>(methodParamTypeNames.length);
+ for (String methodParamName : methodParamTypeNames) {
+ if (!methodParamName.equals("")) {
+ methodParamTypesList.add(nameToType(methodParamName));
+ }
+ }
+ methodParamTypes = methodParamTypesList.toArray(new Class<?>[]{});
+ }
+
+ @Override
+ public String toString() {
+ return className + "#" + methodName;
+ }
+ }
+
+ /**
+ * Used to call the real (non mocked) method.
+ */
+ private static class SuperMethodCall implements InvocationHandlerAdapter.SuperMethod {
+ private final SelfCallInfo selfCallInfo;
+ private final Method origin;
+ private final WeakReference<Object> marker;
+ private final Object[] arguments;
+
+ private SuperMethodCall(SelfCallInfo selfCallInfo, Method origin, Object marker,
+ Object[] arguments) {
+ this.selfCallInfo = selfCallInfo;
+ this.origin = origin;
+ this.marker = new WeakReference(marker);
+ this.arguments = arguments;
+ }
+
+ /**
+ * 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(marker.get());
+ return tryInvoke(origin, 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-extended/src/main/java/com/android/dx/mockito/inline/extended/ExtendedMockito.java b/dexmaker-mockito-inline-extended/src/main/java/com/android/dx/mockito/inline/extended/ExtendedMockito.java
new file mode 100644
index 0000000..2d8acc8
--- /dev/null
+++ b/dexmaker-mockito-inline-extended/src/main/java/com/android/dx/mockito/inline/extended/ExtendedMockito.java
@@ -0,0 +1,444 @@
+/*
+ * Copyright (C) 2018 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.extended;
+
+import org.mockito.InOrder;
+import org.mockito.MockSettings;
+import org.mockito.Mockito;
+import org.mockito.internal.matchers.LocalizedMatcher;
+import org.mockito.internal.progress.ArgumentMatcherStorageImpl;
+import org.mockito.stubbing.Answer;
+import org.mockito.verification.VerificationMode;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+
+import static com.android.dx.mockito.inline.InlineDexmakerMockMaker.onSpyInProgressInstance;
+import static com.android.dx.mockito.inline.InlineStaticMockMaker.onMethodCallDuringVerification;
+import static org.mockito.internal.progress.ThreadSafeMockingProgress.mockingProgress;
+
+/**
+ * Mockito extended with the ability to stub static methods.
+ * <p>E.g.
+ * <pre>
+ * private class C {
+ * static int staticMethod(String arg) {
+ * return 23;
+ * }
+ * }
+ *
+ * {@literal @}Test
+ * public void test() {
+ * // static mocking
+ * MockitoSession session = mockitoSession().staticSpy(C.class).startMocking();
+ * try {
+ * doReturn(42).when(() -> {return C.staticMethod(eq("Arg"));});
+ * assertEquals(42, C.staticMethod("Arg"));
+ * verify(() -> C.staticMethod(eq("Arg"));
+ * } finally {
+ * session.finishMocking();
+ * }
+ * }
+ * </pre>
+ * <p>It is possible to use this class for instance mocking too. Hence you can use it as a full
+ * replacement for {@link Mockito}.
+ * <p>This is a prototype that is intended to eventually be upstreamed into mockito proper. Some
+ * APIs might change. All such APIs are annotated with {@link UnstableApi}.
+ */
+@UnstableApi
+public class ExtendedMockito extends Mockito {
+ /**
+ * Currently active {@link #mockitoSession() sessions}
+ */
+ private static ArrayList<StaticMockitoSession> sessions = new ArrayList<>();
+
+ /**
+ * Same as {@link Mockito#doAnswer(Answer)} but adds the ability to stub static method calls via
+ * {@link StaticCapableStubber#when(MockedMethod)} and
+ * {@link StaticCapableStubber#when(MockedVoidMethod)}.
+ */
+ public static StaticCapableStubber doAnswer(Answer answer) {
+ return new StaticCapableStubber(Mockito.doAnswer(answer));
+ }
+
+ /**
+ * Same as {@link Mockito#doCallRealMethod()} but adds the ability to stub static method calls
+ * via {@link StaticCapableStubber#when(MockedMethod)} and
+ * {@link StaticCapableStubber#when(MockedVoidMethod)}.
+ */
+ public static StaticCapableStubber doCallRealMethod() {
+ return new StaticCapableStubber(Mockito.doCallRealMethod());
+ }
+
+ /**
+ * Same as {@link Mockito#doNothing()} but adds the ability to stub static method calls via
+ * {@link StaticCapableStubber#when(MockedMethod)} and
+ * {@link StaticCapableStubber#when(MockedVoidMethod)}.
+ */
+ public static StaticCapableStubber doNothing() {
+ return new StaticCapableStubber(Mockito.doNothing());
+ }
+
+ /**
+ * Same as {@link Mockito#doReturn(Object)} but adds the ability to stub static method calls
+ * via {@link StaticCapableStubber#when(MockedMethod)} and
+ * {@link StaticCapableStubber#when(MockedVoidMethod)}.
+ */
+ public static StaticCapableStubber doReturn(Object toBeReturned) {
+ return new StaticCapableStubber(Mockito.doReturn(toBeReturned));
+ }
+
+ /**
+ * Same as {@link Mockito#doReturn(Object, Object...)} but adds the ability to stub static
+ * method calls via {@link StaticCapableStubber#when(MockedMethod)} and
+ * {@link StaticCapableStubber#when(MockedVoidMethod)}.
+ */
+ public static StaticCapableStubber doReturn(Object toBeReturned, Object... toBeReturnedNext) {
+ return new StaticCapableStubber(Mockito.doReturn(toBeReturned, toBeReturnedNext));
+ }
+
+ /**
+ * Same as {@link Mockito#doThrow(Class)} but adds the ability to stub static method calls via
+ * {@link StaticCapableStubber#when(MockedMethod)} and
+ * {@link StaticCapableStubber#when(MockedVoidMethod)}.
+ */
+ public static StaticCapableStubber doThrow(Class<? extends Throwable> toBeThrown) {
+ return new StaticCapableStubber(Mockito.doThrow(toBeThrown));
+ }
+
+ /**
+ * Same as {@link Mockito#doThrow(Class, Class...)} but adds the ability to stub static method
+ * calls via {@link StaticCapableStubber#when(MockedMethod)} and
+ * {@link StaticCapableStubber#when(MockedVoidMethod)}.
+ */
+ @SafeVarargs
+ public static StaticCapableStubber doThrow(Class<? extends Throwable> toBeThrown,
+ Class<? extends Throwable>... toBeThrownNext) {
+ return new StaticCapableStubber(Mockito.doThrow(toBeThrown, toBeThrownNext));
+ }
+
+ /**
+ * Same as {@link Mockito#doThrow(Throwable...)} but adds the ability to stub static method
+ * calls via {@link StaticCapableStubber#when(MockedMethod)} and
+ * {@link StaticCapableStubber#when(MockedVoidMethod)}.
+ */
+ public static StaticCapableStubber doThrow(Throwable... toBeThrown) {
+ return new StaticCapableStubber(Mockito.doThrow(toBeThrown));
+ }
+
+ /**
+ * Many methods of mockito take mock objects. To be able to call the same methods for static
+ * mocking, this method gets a marker object that can be used instead.
+ *
+ * @param clazz The class object the marker should be crated for
+ * @return A marker object. This should not be used directly. It can only be passed into other
+ * ExtendedMockito methods.
+ * @see #inOrder(Object...)
+ * @see #clearInvocations(Object...)
+ * @see #ignoreStubs(Object...)
+ * @see #mockingDetails(Object)
+ * @see #reset(Object[])
+ * @see #verifyNoMoreInteractions(Object...)
+ * @see #verifyZeroInteractions(Object...)
+ */
+ @UnstableApi
+ @SuppressWarnings("unchecked")
+ public static <T> T staticMockMarker(Class<T> clazz) {
+ for (StaticMockitoSession session : sessions) {
+ T marker = session.staticMockMarker(clazz);
+
+ if (marker != null) {
+ return marker;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Same as {@link #staticMockMarker(Class)} but for multiple classes at once.
+ */
+ @UnstableApi
+ public static Object[] staticMockMarker(Class<?>... clazz) {
+ Object[] markers = new Object[clazz.length];
+
+ for (int i = 0; i < clazz.length; i++) {
+ for (StaticMockitoSession session : sessions) {
+ markers[i] = session.staticMockMarker(clazz[i]);
+
+ if (markers[i] != null) {
+ break;
+ }
+ }
+
+ if (markers[i] == null) {
+ return null;
+ }
+ }
+
+ return markers;
+ }
+
+ /**
+ * Make an existing object a spy.
+ *
+ * <p>This does <u>not</u> clone the existing objects. If a method is stubbed on a spy
+ * converted by this method all references to the already existing object will be affected by
+ * the stubbing.
+ *
+ * @param toMock The existing object to convert into a spy
+ */
+ @UnstableApi
+ @SuppressWarnings("CheckReturnValue")
+ public static void spyOn(Object toMock) {
+ if (onSpyInProgressInstance.get() != null) {
+ throw new IllegalStateException("Cannot set up spying on an existing object while "
+ + "setting up spying for another existing object");
+ }
+
+ onSpyInProgressInstance.set(toMock);
+ try {
+ spy(toMock);
+ } finally {
+ onSpyInProgressInstance.remove();
+ }
+ }
+
+ /**
+ * To be used for static mocks/spies in place of {@link Mockito#verify(Object)} when calling
+ * void methods.
+ * <p>E.g.
+ * <pre>
+ * private class C {
+ * void instanceMethod(String arg) {}
+ * static void staticMethod(String arg) {}
+ * }
+ *
+ * {@literal @}Test
+ * public void test() {
+ * // instance mocking
+ * C mock = mock(C.class);
+ * mock.instanceMethod("Hello");
+ * verify(mock).mockedVoidInstanceMethod(eq("Hello"));
+ *
+ * // static mocking
+ * MockitoSession session = mockitoSession().staticMock(C.class).startMocking();
+ * C.staticMethod("World");
+ * verify(() -> C.staticMethod(eq("World"));
+ * session.finishMocking();
+ * }
+ * </pre>
+ */
+ public static void verify(MockedVoidMethod method) {
+ verify(method, times(1));
+ }
+
+ /**
+ * To be used for static mocks/spies in place of {@link Mockito#verify(Object)}.
+ * <p>E.g. (please notice the 'return' in the lambda when verifying the static call)
+ * <pre>
+ * private class C {
+ * int instanceMethod(String arg) {
+ * return 1;
+ * }
+ *
+ * int static staticMethod(String arg) {
+ * return 2;
+ * }
+ * }
+ *
+ * {@literal @}Test
+ * public void test() {
+ * // instance mocking
+ * C mock = mock(C.class);
+ * mock.instanceMethod("Hello");
+ * verify(mock).mockedVoidInstanceMethod(eq("Hello"));
+ *
+ * // static mocking
+ * MockitoSession session = mockitoSession().staticMock(C.class).startMocking();
+ * C.staticMethod("World");
+ * verify(() -> <b>{return</b> C.staticMethod(eq("World")<b>;}</b>);
+ * session.finishMocking();
+ * }
+ * </pre>
+ */
+ @UnstableApi
+ public static void verify(MockedMethod method) {
+ verify(method, times(1));
+ }
+
+ /**
+ * To be used for static mocks/spies in place of
+ * {@link Mockito#verify(Object, VerificationMode)} when calling void methods.
+ *
+ * @see #verify(MockedVoidMethod)
+ */
+ @UnstableApi
+ public static void verify(MockedVoidMethod method, VerificationMode mode) {
+ verifyInt(method, mode, null);
+ }
+
+ /**
+ * To be used for static mocks/spies in place of
+ * {@link Mockito#verify(Object, VerificationMode)}.
+ *
+ * @see #verify(MockedMethod)
+ */
+ @UnstableApi
+ public static void verify(MockedMethod method, VerificationMode mode) {
+ verify((MockedVoidMethod) method::get, mode);
+ }
+
+ /**
+ * Same as {@link Mockito#inOrder(Object...)} but adds the ability to verify static method
+ * calls via {@link StaticInOrder#verify(MockedMethod)},
+ * {@link StaticInOrder#verify(MockedVoidMethod)},
+ * {@link StaticInOrder#verify(MockedMethod, VerificationMode)}, and
+ * {@link StaticInOrder#verify(MockedVoidMethod, VerificationMode)}.
+ * <p>To verify static method calls, the result of {@link #staticMockMarker(Class)} has to be
+ * passed to the {@code mocksAndMarkers} parameter. It is possible to mix static and instance
+ * mocking.
+ */
+ @UnstableApi
+ public static StaticInOrder inOrder(Object... mocksAndMarkers) {
+ return new StaticInOrder(Mockito.inOrder(mocksAndMarkers));
+ }
+
+ /**
+ * Same as {@link Mockito#mockitoSession()} but adds the ability to mock static methods
+ * calls via {@link StaticMockitoSessionBuilder#mockStatic(Class)},
+ * {@link StaticMockitoSessionBuilder#mockStatic(Class, Answer)}, and {@link
+ * StaticMockitoSessionBuilder#mockStatic(Class, MockSettings)};
+ * <p>All mocking spying will be removed once the session is finished.
+ */
+ public static StaticMockitoSessionBuilder mockitoSession() {
+ return new StaticMockitoSessionBuilder(Mockito.mockitoSession());
+ }
+
+ /**
+ * Common implementation of verification of static method calls.
+ *
+ * @param method The static method call to be verified
+ * @param mode The verification mode
+ * @param instanceInOrder If set, the {@link StaticInOrder} object
+ */
+ @SuppressWarnings({"CheckReturnValue", "MockitoUsage", "unchecked"})
+ static void verifyInt(MockedVoidMethod method, VerificationMode mode, InOrder
+ instanceInOrder) {
+ if (onMethodCallDuringVerification.get() != null) {
+ throw new IllegalStateException("Verification is already in progress on this "
+ + "thread.");
+ }
+
+ ArrayList<Method> verifications = new ArrayList<>();
+
+ /* Set up callback that is triggered when the next static method is called on this thread.
+ *
+ * This is necessary as we don't know which class the method will be called on. Once the
+ * call is intercepted this will
+ * 1. Remove all matchers (e.g. eq(), any()) from the matcher stack
+ * 2. Call verify on the marker for the class
+ * 3. Add the markers back to the stack
+ */
+ onMethodCallDuringVerification.set((clazz, verifiedMethod) -> {
+ // TODO: O holy reflection! Let's hope we can integrate this better.
+ try {
+ ArgumentMatcherStorageImpl argMatcherStorage = (ArgumentMatcherStorageImpl)
+ mockingProgress().getArgumentMatcherStorage();
+ List<LocalizedMatcher> matchers;
+
+ // Matcher are called before verify, hence remove the from the storage
+ Method resetStackMethod
+ = argMatcherStorage.getClass().getDeclaredMethod("resetStack");
+ resetStackMethod.setAccessible(true);
+
+ matchers = (List<LocalizedMatcher>) resetStackMethod.invoke(argMatcherStorage);
+
+ if (instanceInOrder == null) {
+ verify(staticMockMarker(clazz), mode);
+ } else {
+ instanceInOrder.verify(staticMockMarker(clazz), mode);
+ }
+
+ // Add the matchers back after verify is called
+ Field matcherStackField
+ = argMatcherStorage.getClass().getDeclaredField("matcherStack");
+ matcherStackField.setAccessible(true);
+
+ Method pushMethod = matcherStackField.getType().getDeclaredMethod("push",
+ Object.class);
+
+ for (LocalizedMatcher matcher : matchers) {
+ pushMethod.invoke(matcherStackField.get(argMatcherStorage), matcher);
+ }
+ } catch (NoSuchFieldException | NoSuchMethodException | IllegalAccessException
+ | InvocationTargetException | ClassCastException e) {
+ throw new Error("Reflection failed. Do you use a compatible version of "
+ + "mockito?", e);
+ }
+
+ verifications.add(verifiedMethod);
+ });
+ try {
+ try {
+ // Trigger the method call. This call will be intercepted and trigger the
+ // onMethodCallDuringVerification callback.
+ method.run();
+ } catch (Throwable t) {
+ if (t instanceof RuntimeException) {
+ throw (RuntimeException) t;
+ } else if (t instanceof Error) {
+ throw (Error) t;
+ }
+ throw new RuntimeException(t);
+ }
+
+ if (verifications.isEmpty()) {
+ // Make sure something was intercepted
+ throw new IllegalArgumentException("Nothing was verified. Does the lambda call "
+ + "a static method on a 'static' mock/spy ?");
+ } else if (verifications.size() > 1) {
+ // A lambda might call several methods. In this case it is not clear what should
+ // be verified. Hence throw an error.
+ throw new IllegalArgumentException("Multiple intercepted calls on methods "
+ + verifications);
+ }
+ } finally {
+ onMethodCallDuringVerification.remove();
+ }
+ }
+
+ /**
+ * Register a new session.
+ *
+ * @param session Session to register
+ */
+ static void addSession(StaticMockitoSession session) {
+ sessions.add(session);
+ }
+
+ /**
+ * Remove a finished session.
+ *
+ * @param session Session to remove
+ */
+ static void removeSession(StaticMockitoSession session) {
+ sessions.remove(session);
+ }
+}
diff --git a/dexmaker-mockito-inline-extended/src/main/java/com/android/dx/mockito/inline/extended/MockedMethod.java b/dexmaker-mockito-inline-extended/src/main/java/com/android/dx/mockito/inline/extended/MockedMethod.java
new file mode 100644
index 0000000..accc65a
--- /dev/null
+++ b/dexmaker-mockito-inline-extended/src/main/java/com/android/dx/mockito/inline/extended/MockedMethod.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2018 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.extended;
+
+/**
+ * A call to a method that should be stubbed / verified.
+ *
+ * @param <T> return type of the method
+ */
+@UnstableApi
+public interface MockedMethod<T> {
+ T get() throws Throwable;
+}
diff --git a/dexmaker-mockito-inline-extended/src/main/java/com/android/dx/mockito/inline/extended/MockedVoidMethod.java b/dexmaker-mockito-inline-extended/src/main/java/com/android/dx/mockito/inline/extended/MockedVoidMethod.java
new file mode 100644
index 0000000..e84b58d
--- /dev/null
+++ b/dexmaker-mockito-inline-extended/src/main/java/com/android/dx/mockito/inline/extended/MockedVoidMethod.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2018 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.extended;
+
+/**
+ * A call to a void method that should be stubbed / verified.
+ */
+@UnstableApi
+public interface MockedVoidMethod {
+ void run() throws Throwable;
+}
diff --git a/dexmaker-mockito-inline-extended/src/main/java/com/android/dx/mockito/inline/extended/StaticCapableStubber.java b/dexmaker-mockito-inline-extended/src/main/java/com/android/dx/mockito/inline/extended/StaticCapableStubber.java
new file mode 100644
index 0000000..0872f82
--- /dev/null
+++ b/dexmaker-mockito-inline-extended/src/main/java/com/android/dx/mockito/inline/extended/StaticCapableStubber.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2018 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.extended;
+
+import org.mockito.stubbing.Answer;
+import org.mockito.stubbing.Stubber;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+
+import static com.android.dx.mockito.inline.InlineStaticMockMaker.onMethodCallDuringStubbing;
+
+/**
+ * Same as {@link Stubber} but supports settings up stubbing of static methods via
+ * {@link #when(MockedMethod)} and {@link #when(MockedVoidMethod)}.
+ */
+@UnstableApi
+public class StaticCapableStubber implements Stubber {
+ private Stubber instanceStubber;
+
+ StaticCapableStubber(Stubber instanceStubber) {
+ this.instanceStubber = instanceStubber;
+ }
+
+ @Override
+ public <T> T when(T mock) {
+ return instanceStubber.when(mock);
+ }
+
+ /**
+ * Common implementation of all {@code doReturn.when} calls.
+ *
+ * @param method The static method to be stubbed
+ */
+ private void whenInt(MockedVoidMethod method) {
+ if (onMethodCallDuringStubbing.get() != null) {
+ throw new IllegalStateException("Stubbing is already in progress on this thread.");
+ }
+
+ ArrayList<Method> stubbingsSetUp = new ArrayList<>();
+
+ /* Set up interception of method. 'method' does not specify what class the stubbing is
+ * set up on. Hence wait until the call is made and intercept it just before the code
+ * is executed. At this time, start the stubbing operation.
+ */
+ onMethodCallDuringStubbing.set((clazz, stubbedMethod) -> {
+ when(ExtendedMockito.staticMockMarker(clazz));
+ stubbingsSetUp.add(stubbedMethod);
+ });
+ try {
+ try {
+ // Call the method. This will be intercepted by onMethodCallDuringStubbing
+ method.run();
+ } catch (Throwable t) {
+ throw new RuntimeException(t);
+ }
+
+ if (stubbingsSetUp.isEmpty()) {
+ // Make sure something was intercepted
+ throw new IllegalArgumentException("Nothing was stubbed. Does the lambda call a"
+ + " static method on a 'static' mock/spy?");
+ } else if (stubbingsSetUp.size() > 1) {
+ // A lambda might call several methods. In this case it is not clear what should
+ // be stubbed. Hence throw an error.
+ throw new IllegalArgumentException("Multiple intercepted calls on method "
+ + stubbingsSetUp);
+ }
+ } finally {
+ onMethodCallDuringStubbing.remove();
+ }
+ }
+
+ /**
+ * Set up stubbing for a static void method.
+ * <pre>
+ * private class C {
+ * void instanceMethod(String arg) {}
+ * static void staticMethod(String arg) {}
+ * }
+ *
+ * {@literal @}Test
+ * public void test() {
+ * // instance mocking
+ * C mock = mock(C.class);
+ * doThrow(Exception.class).when(mock).instanceMethod(eq("Hello));
+ * assertThrows(Exception.class, mock.instanceMethod("Hello"));
+ *
+ * // static mocking
+ * MockitoSession session = mockitoSession().staticMock(C.class).startMocking();
+ * doThrow(Exception.class).when(() -> C.instanceMethod(eq("Hello));
+ * assertThrows(Exception.class, C.staticMethod("Hello"));
+ * session.finishMocking();
+ * }
+ * </pre>
+ *
+ * @param method The method to stub as a lambda. This should only call a single stubbable
+ * static method.
+ */
+ @UnstableApi
+ public void when(MockedVoidMethod method) {
+ whenInt(method);
+ }
+
+ /**
+ * Set up stubbing for a static method.
+ * <pre>
+ * private class C {
+ * int instanceMethod(String arg) {
+ * return 1;
+ * }
+ *
+ * int static staticMethod(String arg) {
+ * return 1;
+ * }
+ * }
+ *
+ * {@literal @}Test
+ * public void test() {
+ * // instance mocking
+ * C mock = mock(C.class);
+ * doReturn(2).when(mock).instanceMethod(eq("Hello));
+ * assertEquals(2, mock.instanceMethod("Hello"));
+ *
+ * // static mocking
+ * MockitoSession session = mockitoSession().staticMock(C.class).startMocking();
+ * doReturn(2).when(() -> C.instanceMethod(eq("Hello));
+ * assertEquals(2, C.staticMethod("Hello"));
+ * session.finishMocking();
+ * }
+ * </pre>
+ *
+ * @param method The method to stub as a lambda. This should only call a single stubbable
+ * static method.
+ * @param <T> Return type of the stubbed method
+ */
+ @UnstableApi
+ public <T> void when(MockedMethod<T> method) {
+ whenInt(method::get);
+ }
+
+ @Override
+ public StaticCapableStubber doThrow(Throwable... toBeThrown) {
+ instanceStubber = instanceStubber.doThrow(toBeThrown);
+ return this;
+ }
+
+ @Override
+ public StaticCapableStubber doThrow(Class<? extends Throwable> toBeThrown) {
+ instanceStubber = instanceStubber.doThrow(toBeThrown);
+ return this;
+ }
+
+ @SafeVarargs
+ @Override
+ public final StaticCapableStubber doThrow(Class<? extends Throwable> toBeThrown,
+ Class<? extends Throwable>... nextToBeThrown) {
+ instanceStubber = instanceStubber.doThrow(toBeThrown, nextToBeThrown);
+ return this;
+ }
+
+ @Override
+ public StaticCapableStubber doAnswer(Answer answer) {
+ instanceStubber = instanceStubber.doAnswer(answer);
+ return this;
+ }
+
+ @Override
+ public StaticCapableStubber doNothing() {
+ instanceStubber = instanceStubber.doNothing();
+ return this;
+ }
+
+ @Override
+ public StaticCapableStubber doReturn(Object toBeReturned) {
+ instanceStubber = instanceStubber.doReturn(toBeReturned);
+ return this;
+ }
+
+ @Override
+ public StaticCapableStubber doReturn(Object toBeReturned, Object... nextToBeReturned) {
+ instanceStubber = instanceStubber.doReturn(toBeReturned, nextToBeReturned);
+ return this;
+ }
+
+ @Override
+ public StaticCapableStubber doCallRealMethod() {
+ instanceStubber = instanceStubber.doCallRealMethod();
+ return this;
+ }
+}
diff --git a/dexmaker-mockito-inline-extended/src/main/java/com/android/dx/mockito/inline/extended/StaticInOrder.java b/dexmaker-mockito-inline-extended/src/main/java/com/android/dx/mockito/inline/extended/StaticInOrder.java
new file mode 100644
index 0000000..312a454
--- /dev/null
+++ b/dexmaker-mockito-inline-extended/src/main/java/com/android/dx/mockito/inline/extended/StaticInOrder.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2018 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.extended;
+
+import org.mockito.InOrder;
+import org.mockito.Mockito;
+import org.mockito.verification.VerificationMode;
+
+/**
+ * Same as {@link InOrder} but adds the ability to verify static method calls via
+ * {@link #verify(MockedMethod)}, {@link #verify(MockedVoidMethod)},
+ * {@link #verify(MockedMethod, VerificationMode)}, and
+ * {@link #verify(MockedVoidMethod, VerificationMode)}.
+ */
+@UnstableApi
+public class StaticInOrder implements InOrder {
+ private final InOrder instanceInOrder;
+
+ StaticInOrder(InOrder inOrder) {
+ instanceInOrder = inOrder;
+ }
+
+ @Override
+ public <T> T verify(T mock) {
+ return instanceInOrder.verify(mock);
+ }
+
+ @Override
+ public <T> T verify(T mock, VerificationMode mode) {
+ return instanceInOrder.verify(mock, mode);
+ }
+
+ /**
+ * To be used for static mocks/spies in place of {@link #verify(Object)} when calling void
+ * methods.
+ * <p>E.g.
+ * <pre>
+ * private class C {
+ * void instanceMethod(String arg) {}
+ * void void staticMethod(String arg) {}
+ * }
+ *
+ * {@literal @}Test
+ * public void test() {
+ * // instance mocking
+ * C mock = mock(C.class);
+ * mock.staticMethod("Hello");
+ * mock.instanceMethod("World");
+ * inOrder().verify(mock).mockedVoidInstanceMethod(eq("Hello"));
+ * inOrder().verify(mock).mockedVoidInstanceMethod(eq("World"));
+ *
+ * // static mocking
+ * MockitoSession session = mockitoSession().staticMock(C.class).startMocking();
+ * C.staticMethod("Hello");
+ * C.staticMethod("World");
+ *
+ * StaticInOrder inOrder = inOrder();
+ * inOrder.verify(() -> C.staticMethod(eq("Hello"));
+ * inOrder.verify(() -> C.staticMethod(eq("World"));
+ * session.finishMocking();
+ * }
+ * </pre>
+ */
+ public void verify(MockedVoidMethod method) {
+ verify(method, Mockito.times(1));
+ }
+
+ /**
+ * To be used for static mocks/spies in place of {@link #verify(Object)}.
+ * <p>E.g.
+ * <pre>
+ * private class C {
+ * int instanceMethod(String arg) {
+ * return 1;
+ * }
+ *
+ * int static staticMethod(String arg) {
+ * return 2;
+ * }
+ * }
+ *
+ * {@literal @}Test
+ * public void test() {
+ * // instance mocking
+ * C mock = mock(C.class);
+ * mock.instanceMethod("Hello");
+ * mock.instanceMethod("World");
+ * inOrder().verify(mock).mockedVoidInstanceMethod(eq("Hello"));
+ * inOrder().verify(mock).mockedVoidInstanceMethod(eq("World"));
+ *
+ * // static mocking
+ * MockitoSession session = mockitoSession().staticMock(C.class).startMocking();
+ * C.staticMethod("Hello");
+ * C.staticMethod("World");
+ *
+ * StaticInOrder inOrder = inOrder();
+ * inOrder.verify(() -> C.staticMethod(eq("Hello"));
+ * inOrder.verify(() -> C.staticMethod(eq("World"));
+ * session.finishMocking();
+ * }
+ * </pre>
+ */
+ @UnstableApi
+ public void verify(MockedMethod method) {
+ verify(method, Mockito.times(1));
+ }
+
+ /**
+ * To be used for static mocks/spies in place of
+ * {@link InOrder#verify(Object, VerificationMode)} when calling void methods.
+ *
+ * @see #verify(MockedVoidMethod)
+ */
+ @UnstableApi
+ public void verify(MockedVoidMethod method, VerificationMode mode) {
+ ExtendedMockito.verifyInt(method, mode, instanceInOrder);
+ }
+
+ /**
+ * To be used for static mocks/spies in place of
+ * {@link InOrder#verify(Object, VerificationMode)}.
+ *
+ * @see #verify(MockedMethod)
+ */
+ @UnstableApi
+ public void verify(MockedMethod method, VerificationMode mode) {
+ verify((MockedVoidMethod) method::get, mode);
+ }
+
+ @Override
+ public void verifyNoMoreInteractions() {
+ instanceInOrder.verifyNoMoreInteractions();
+ }
+}
diff --git a/dexmaker-mockito-inline-extended/src/main/java/com/android/dx/mockito/inline/extended/StaticMocking.java b/dexmaker-mockito-inline-extended/src/main/java/com/android/dx/mockito/inline/extended/StaticMocking.java
new file mode 100644
index 0000000..071e4af
--- /dev/null
+++ b/dexmaker-mockito-inline-extended/src/main/java/com/android/dx/mockito/inline/extended/StaticMocking.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2018 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.extended;
+
+import java.util.function.Supplier;
+
+/**
+ * Data class for a class and the way to create the marker object for the class. As all
+ * invocations are routed to the marker the way we create the marker also determines the other
+ * properties of the mock.
+ */
+class StaticMocking<T> {
+ final Class<T> clazz;
+ final Supplier<T> markerSupplier;
+
+ StaticMocking(Class<T> clazz, Supplier<T> markerSupplier) {
+ this.clazz = clazz;
+ this.markerSupplier = markerSupplier;
+ }
+}
diff --git a/dexmaker-mockito-inline-extended/src/main/java/com/android/dx/mockito/inline/extended/StaticMockitoSession.java b/dexmaker-mockito-inline-extended/src/main/java/com/android/dx/mockito/inline/extended/StaticMockitoSession.java
new file mode 100644
index 0000000..851d4a4
--- /dev/null
+++ b/dexmaker-mockito-inline-extended/src/main/java/com/android/dx/mockito/inline/extended/StaticMockitoSession.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2018 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.extended;
+
+import org.mockito.Mockito;
+import org.mockito.MockitoSession;
+import org.mockito.quality.Strictness;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+import static com.android.dx.mockito.inline.InlineStaticMockMaker.mockingInProgressClass;
+
+/**
+ * Same as {@link MockitoSession} but used when static methods are also stubbed.
+ */
+@UnstableApi
+public class StaticMockitoSession implements MockitoSession {
+ /**
+ * For each class where static mocking is enabled there is one marker object.
+ */
+ private static final HashMap<Class, Object> classToMarker = new HashMap<>();
+
+ private final MockitoSession instanceSession;
+ private final ArrayList<Class<?>> staticMocks = new ArrayList<>(0);
+
+ StaticMockitoSession(MockitoSession instanceSession) {
+ ExtendedMockito.addSession(this);
+ this.instanceSession = instanceSession;
+ }
+
+ @Override
+ public void setStrictness(Strictness strictness) {
+ instanceSession.setStrictness(strictness);
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p><b>Extension:</b> This also resets all stubbing of static methods set up in the
+ * {@link ExtendedMockito#mockitoSession() builder} of the session.
+ */
+ @Override
+ public void finishMocking() {
+ finishMocking(null);
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p><b>Extension:</b> This also resets all stubbing of static methods set up in the
+ * {@link ExtendedMockito#mockitoSession() builder} of the session.
+ */
+ @Override
+ public void finishMocking(Throwable failure) {
+ try {
+ instanceSession.finishMocking(failure);
+ } finally {
+ for (Class<?> clazz : staticMocks) {
+ mockingInProgressClass.set(clazz);
+ try {
+ Mockito.reset(ExtendedMockito.staticMockMarker(clazz));
+ } finally {
+ mockingInProgressClass.remove();
+ }
+ classToMarker.remove(clazz);
+ }
+
+ ExtendedMockito.removeSession(this);
+ }
+ }
+
+ /**
+ * Init mocking for a class.
+ *
+ * @param mocking Description and settings of the mocking
+ * @param <T> The class to mock
+ */
+ <T> void mockStatic(StaticMocking<T> mocking) {
+ if (ExtendedMockito.staticMockMarker(mocking.clazz) != null) {
+ throw new IllegalArgumentException(mocking.clazz + " is already mocked");
+ }
+
+ mockingInProgressClass.set(mocking.clazz);
+ try {
+ classToMarker.put(mocking.clazz, mocking.markerSupplier.get());
+ } finally {
+ mockingInProgressClass.remove();
+ }
+
+ staticMocks.add(mocking.clazz);
+ }
+
+ /**
+ * Get marker for a mocked/spies class or {@code null}.
+ *
+ * @param clazz The class that is mocked
+ * @return marker for a mocked class or {@code null} if class is not mocked in this session
+ * @see ExtendedMockito#staticMockMarker(Class)
+ */
+ @SuppressWarnings("unchecked")
+ <T> T staticMockMarker(Class<T> clazz) {
+ return (T) classToMarker.get(clazz);
+ }
+}
diff --git a/dexmaker-mockito-inline-extended/src/main/java/com/android/dx/mockito/inline/extended/StaticMockitoSessionBuilder.java b/dexmaker-mockito-inline-extended/src/main/java/com/android/dx/mockito/inline/extended/StaticMockitoSessionBuilder.java
new file mode 100644
index 0000000..89d619f
--- /dev/null
+++ b/dexmaker-mockito-inline-extended/src/main/java/com/android/dx/mockito/inline/extended/StaticMockitoSessionBuilder.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2018 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.extended;
+
+import org.mockito.MockSettings;
+import org.mockito.Mockito;
+import org.mockito.exceptions.misusing.UnfinishedMockingSessionException;
+import org.mockito.quality.Strictness;
+import org.mockito.session.MockitoSessionBuilder;
+import org.mockito.session.MockitoSessionLogger;
+import org.mockito.stubbing.Answer;
+
+import java.util.ArrayList;
+
+/**
+ * Same as {@link MockitoSessionBuilder} but adds the ability to stub static methods
+ * calls via {@link #mockStatic(Class)}, {@link #mockStatic(Class, Answer)}, and
+ * {@link #mockStatic(Class, MockSettings)};
+ * <p>All mocks/spies will be reset once the session is finished.
+ */
+@UnstableApi
+public class StaticMockitoSessionBuilder implements MockitoSessionBuilder {
+ private final ArrayList<StaticMocking> staticMockings = new ArrayList<>(0);
+ private MockitoSessionBuilder instanceSessionBuilder;
+
+ StaticMockitoSessionBuilder(MockitoSessionBuilder instanceSessionBuilder) {
+ this.instanceSessionBuilder = instanceSessionBuilder;
+ }
+
+ /**
+ * Sets up mocking for all static methods of a class. All methods will return the default value.
+ * <p>This changes the behavior of <u>all</u> static methods calls for <u>all</u>
+ * invocations. In most cases using {@link #spyStatic(Class)} and stubbing only a few
+ * methods can be used.
+ *
+ * @param clazz The class to set up static mocking for
+ * @return This builder
+ */
+ @UnstableApi
+ public <T> StaticMockitoSessionBuilder mockStatic(Class<T> clazz) {
+ staticMockings.add(new StaticMocking<>(clazz, () -> Mockito.mock(clazz)));
+ return this;
+ }
+
+ /**
+ * Sets up mocking for sall tatic methods of a class. All methods will call the {@code
+ * defaultAnswer}.
+ * <p>This changes the behavior of <u>all</u> static methods calls for <u>all</u>
+ * invocations. In most cases using {@link #spyStatic(Class)} and stubbing only a few
+ * methods can be used.
+ *
+ * @param clazz The class to set up static mocking for
+ * @param defaultAnswer The answer to return by default
+ * @return This builder
+ */
+ @UnstableApi
+ public <T> StaticMockitoSessionBuilder mockStatic(Class<T> clazz, Answer defaultAnswer) {
+ staticMockings.add(new StaticMocking<>(clazz, () -> Mockito.mock(clazz, defaultAnswer)));
+ return this;
+ }
+
+ /**
+ * Sets up mocking for all static methods of a class with custom {@link MockSettings}.
+ * <p>This changes the behavior of <u>all</u> static methods calls for <u>all</u>
+ * invocations. In most cases using {@link #spyStatic(Class)} and stubbing only a few
+ * methods can be used.
+ *
+ * @param clazz The class to set up static mocking for
+ * @param settings Settings used to set up the mock.
+ * @return This builder
+ */
+ @UnstableApi
+ public <T> StaticMockitoSessionBuilder mockStatic(Class<T> clazz, MockSettings settings) {
+ staticMockings.add(new StaticMocking<>(clazz, () -> Mockito.mock(clazz, settings)));
+ return this;
+ }
+
+ /**
+ * Sets up spying for static methods of a class.
+ *
+ * @param clazz The class to set up static spying for
+ * @return This builder
+ */
+ @UnstableApi
+ public <T> StaticMockitoSessionBuilder spyStatic(Class<T> clazz) {
+ staticMockings.add(new StaticMocking<>(clazz, () -> Mockito.spy(clazz)));
+ return this;
+ }
+
+ @Override
+ public StaticMockitoSessionBuilder initMocks(Object testClassInstance) {
+ instanceSessionBuilder = instanceSessionBuilder.initMocks(testClassInstance);
+ return this;
+ }
+
+ @Override
+ public StaticMockitoSessionBuilder initMocks(Object... testClassInstances) {
+ instanceSessionBuilder = instanceSessionBuilder.initMocks(testClassInstances);
+ return this;
+ }
+
+ @Override
+ public StaticMockitoSessionBuilder name(String name) {
+ instanceSessionBuilder = instanceSessionBuilder.name(name);
+ return this;
+ }
+
+ @Override
+ public StaticMockitoSessionBuilder strictness(Strictness strictness) {
+ instanceSessionBuilder = instanceSessionBuilder.strictness(strictness);
+ return this;
+ }
+
+ @Override
+ public StaticMockitoSessionBuilder logger(MockitoSessionLogger logger) {
+ instanceSessionBuilder = instanceSessionBuilder.logger(logger);
+ return this;
+ }
+
+ @Override
+ public StaticMockitoSession startMocking() throws UnfinishedMockingSessionException {
+ StaticMockitoSession session
+ = new StaticMockitoSession(instanceSessionBuilder.startMocking());
+ try {
+ for (StaticMocking mocking : staticMockings) {
+ session.mockStatic((StaticMocking<?>) mocking);
+ }
+ } catch (Throwable t) {
+ try {
+ session.finishMocking();
+ } catch (Throwable ignored) {
+ // suppress all failures
+ }
+ throw t;
+ }
+
+ return session;
+ }
+}
diff --git a/dexmaker-mockito-inline-extended/src/main/java/com/android/dx/mockito/inline/extended/UnstableApi.java b/dexmaker-mockito-inline-extended/src/main/java/com/android/dx/mockito/inline/extended/UnstableApi.java
new file mode 100644
index 0000000..379b2fa
--- /dev/null
+++ b/dexmaker-mockito-inline-extended/src/main/java/com/android/dx/mockito/inline/extended/UnstableApi.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2018 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.extended;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * The API is not a mockito API and there is a chance in might change in future versions. Some
+ * classes inherit from Mockito classes. In this case the class name is not stable, but the
+ * inherited methods are.
+ */
+@Retention(RetentionPolicy.CLASS)
+@Documented
+public @interface UnstableApi {
+}
diff --git a/dexmaker-mockito-inline-extended/src/main/jni/staticjvmtiagent/agent.cc b/dexmaker-mockito-inline-extended/src/main/jni/staticjvmtiagent/agent.cc
new file mode 100644
index 0000000..3956893
--- /dev/null
+++ b/dexmaker-mockito-inline-extended/src/main/jni/staticjvmtiagent/agent.cc
@@ -0,0 +1,833 @@
+/*
+ * Copyright (C) 2018 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 <slicer/dex_ir.h>
+#include <slicer/code_ir.h>
+#include <slicer/dex_ir_builder.h>
+#include <slicer/dex_utf8.h>
+#include <slicer/writer.h>
+#include <slicer/reader.h>
+#include <slicer/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 != nullptr) {
+ // Even reading the classData array is expensive as the data is only generated when the
+ // memory is touched. Hence call JvmtiAgent#shouldTransform to check if we need to transform
+ // the class.
+ jclass cls = env->GetObjectClass(sTransformer);
+ jmethodID shouldTransformMethod = env->GetMethodID(cls, "shouldTransform",
+ "(Ljava/lang/Class;)Z");
+
+ jboolean shouldTransform = env->CallBooleanMethod(sTransformer, shouldTransformMethod,
+ classBeingRedefined);
+ if (!shouldTransform) {
+ return;
+ }
+
+ // 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
+ jmethodID runTransformersMethod = env->GetMethodID(cls, "runTransformers",
+ "(Ljava/lang/ClassLoader;"
+ "Ljava/lang/String;"
+ "Ljava/lang/Class;"
+ "Ljava/security/ProtectionDomain;"
+ "[B)[B");
+
+ jbyteArray transformedArr = (jbyteArray) env->CallObjectMethod(sTransformer,
+ runTransformersMethod,
+ loader, nameStr,
+ classBeingRedefined,
+ protectionDomain,
+ isolatedClassArr);
+
+ // Set transformed byte code
+ if (!env->ExceptionOccurred() && transformedArr != nullptr) {
+ *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 == nullptr) {
+ 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 & kAccStatic) != 0)
+ && !(((method->access_flags & (kAccPrivate | kAccBridge | kAccNative)) != 0)
+ || (Utf8Cmp(methodName->c_str(), "<clinit>") == 0)
+ || (strncmp(type.c_str(), "java.", 5) == 0
+ && (method->access_flags & (kAccPrivate | kAccPublic | kAccProtected))
+ == 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_StaticClassTransformer_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* 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* 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;
+ }
+ /*
+ static long method_original(int param1, long param2, String param3) {
+ foo();
+ return bar();
+ }
+
+ static long method_transformed(int param1, long param2, String param3) {
+ // MockMethodDispatcher dispatcher = MockMethodDispatcher.get(idStr, this);
+ const-string v0, "65463hg34t"
+ const v1, 0
+ 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)"
+ const v2, 0
+ 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(methodDesc --as this parameter--,
+ // origin, arguments);
+ const-string v3 "fully.qualified.ClassName#original_method(int, long, String)"
+ 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
+ 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 firstArg = 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 != nullptr) {
+ 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);
+
+ addInstr(c, fi, OP_CONST_STRING, {v0, c.Alloc<String>(id, id->orig_index)});
+ addInstr(c, fi, OP_CONST, {v1, c.Alloc<Const32>(0)});
+ 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_CONST, {v2, c.Alloc<Const32>(0)});
+ 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 = firstArg;
+
+ 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});
+ }
+ }
+
+ // NASTY Hack: Push in method name as "mock"
+ addInstr(c, fi, OP_CONST_STRING,
+ {v3, c.Alloc<String>(methodDesc, methodDesc->orig_index)});
+ 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);
+
+ if (numParams > 0) {
+ int argReg = firstArg;
+
+ 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, nullptr);
+ 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_StaticJvmtiAgent_nativeRegisterTransformerHook(JNIEnv* env,
+ jobject thiz) {
+ sTransformer = env->NewGlobalRef(thiz);
+ }
+
+ // Unregister transformer hook
+ extern "C" JNIEXPORT void JNICALL
+ Java_com_android_dx_mockito_inline_StaticJvmtiAgent_nativeUnregisterTransformerHook(JNIEnv* env,
+ jobject thiz) {
+ env->DeleteGlobalRef(sTransformer);
+ sTransformer = nullptr;
+ }
+
+ // Triggers retransformation of classes via this file's Transform method
+ extern "C" JNIEXPORT void JNICALL
+ Java_com_android_dx_mockito_inline_StaticJvmtiAgent_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);
+ }
+ }
+
+ static jvmtiFrameInfo* frameToInspect;
+ static std::string calledClass;
+
+ // 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
+ InspectClass(jvmtiEnv* jvmtiEnv,
+ JNIEnv* env,
+ jclass classBeingRedefined,
+ jobject loader,
+ const char* name,
+ jobject protectionDomain,
+ jint classDataLen,
+ const unsigned char* classData,
+ jint* newClassDataLen,
+ unsigned char** newClassData) {
+ calledClass = "none";
+
+ Reader reader(classData, classDataLen);
+
+ char *calledMethodName;
+ char *calledMethodSignature;
+ jvmtiError error = jvmtiEnv->GetMethodName(frameToInspect->method, &calledMethodName,
+ &calledMethodSignature, nullptr);
+ if (error != JVMTI_ERROR_NONE) {
+ return;
+ }
+
+ u4 index = reader.FindClassIndex(ClassNameToDescriptor(name).c_str());
+ reader.CreateClassIr(index);
+ std::shared_ptr<ir::DexFile> class_ir = reader.GetIr();
+
+ for (auto& method : class_ir->encoded_methods) {
+ if (Utf8Cmp(method->decl->name->c_str(), calledMethodName) == 0
+ && Utf8Cmp(method->decl->prototype->Signature().c_str(), calledMethodSignature) == 0) {
+ CodeIr method_ir(method.get(), class_ir);
+
+ for (auto instruction : method_ir.instructions) {
+ Bytecode* bytecode = dynamic_cast<Bytecode*>(instruction);
+ if (bytecode != nullptr && bytecode->offset == frameToInspect->location) {
+ Method *method = bytecode->CastOperand<Method>(1);
+ calledClass = method->ir_method->parent->Decl().c_str();
+
+ goto exit;
+ }
+ }
+ }
+ }
+
+ exit:
+ free(calledMethodName);
+ free(calledMethodSignature);
+ }
+
+#define GOTO_ON_ERROR(label) \
+ if (error != JVMTI_ERROR_NONE) { \
+ goto label; \
+ }
+
+// stack frame of the caller if method was called directly
+#define DIRECT_CALL_STACK_FRAME (6)
+
+// stack frame of the caller if method was called as 'real method'
+#define REALMETHOD_CALL_STACK_FRAME (23)
+
+ extern "C" JNIEXPORT jstring JNICALL
+ Java_com_android_dx_mockito_inline_StaticMockMethodAdvice_nativeGetCalledClassName(JNIEnv* env,
+ jclass klass) {
+ JavaVM *vm;
+ jint jvmError = env->GetJavaVM(&vm);
+ if (jvmError != JNI_OK) {
+ return nullptr;
+ }
+
+ jvmtiEnv *jvmtiEnv;
+ jvmError = vm->GetEnv(reinterpret_cast<void**>(&jvmtiEnv), JVMTI_VERSION_1_2);
+ if (jvmError != JNI_OK) {
+ return nullptr;
+ }
+
+ jvmtiCapabilities caps;
+ memset(&caps, 0, sizeof(caps));
+ caps.can_retransform_classes = 1;
+
+ jvmtiError error = jvmtiEnv->AddCapabilities(&caps);
+ GOTO_ON_ERROR(unregister_env_and_exit);
+
+ jvmtiEventCallbacks cb;
+ memset(&cb, 0, sizeof(cb));
+ cb.ClassFileLoadHook = InspectClass;
+
+ jvmtiFrameInfo frameInfo[REALMETHOD_CALL_STACK_FRAME + 1];
+ jint numFrames;
+ error = jvmtiEnv->GetStackTrace(nullptr, 0, REALMETHOD_CALL_STACK_FRAME + 1, frameInfo,
+ &numFrames);
+ GOTO_ON_ERROR(unregister_env_and_exit);
+
+ // Method might be called directly or as 'real method' (see
+ // StaticMockMethodAdvice.SuperMethodCall#invoke). Hence the real caller might be in stack
+ // frame DIRECT_CALL_STACK_FRAME for a direct call or REALMETHOD_CALL_STACK_FRAME for a
+ // call through the 'real method' mechanism.
+ int callingFrameNum;
+ if (numFrames < REALMETHOD_CALL_STACK_FRAME) {
+ callingFrameNum = DIRECT_CALL_STACK_FRAME;
+ } else {
+ char *directCallMethodName;
+
+ jvmtiEnv->GetMethodName(frameInfo[DIRECT_CALL_STACK_FRAME].method,
+ &directCallMethodName, nullptr, nullptr);
+ if (strcmp(directCallMethodName, "invoke") == 0) {
+ callingFrameNum = REALMETHOD_CALL_STACK_FRAME;
+ } else {
+ callingFrameNum = DIRECT_CALL_STACK_FRAME;
+ }
+ }
+
+ jclass callingClass;
+ error = jvmtiEnv->GetMethodDeclaringClass(frameInfo[callingFrameNum].method, &callingClass);
+ GOTO_ON_ERROR(unregister_env_and_exit);
+
+ error = jvmtiEnv->SetEventCallbacks(&cb, sizeof(cb));
+ GOTO_ON_ERROR(unregister_env_and_exit);
+
+ error = jvmtiEnv->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_CLASS_FILE_LOAD_HOOK,
+ nullptr);
+ GOTO_ON_ERROR(unset_cb_and_exit);
+
+ frameToInspect = &frameInfo[callingFrameNum];
+ error = jvmtiEnv->RetransformClasses(1, &callingClass);
+ GOTO_ON_ERROR(disable_hook_and_exit);
+
+ disable_hook_and_exit:
+ jvmtiEnv->SetEventNotificationMode(JVMTI_DISABLE, JVMTI_EVENT_CLASS_FILE_LOAD_HOOK,
+ nullptr);
+
+ unset_cb_and_exit:
+ memset(&cb, 0, sizeof(cb));
+ jvmtiEnv->SetEventCallbacks(&cb, sizeof(cb));
+
+ unregister_env_and_exit:
+ jvmtiEnv->DisposeEnvironment();
+
+ if (error != JVMTI_ERROR_NONE) {
+ return nullptr;
+ }
+
+ return env->NewStringUTF(calledClass.c_str());
+ }
+
+} // namespace com_android_dx_mockito_inline
+
diff --git a/dexmaker-mockito-inline-tests/AndroidManifest.xml b/dexmaker-mockito-inline-tests/AndroidManifest.xml
deleted file mode 100644
index 44afd30..0000000
--- a/dexmaker-mockito-inline-tests/AndroidManifest.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-<manifest package="com.android.dexmaker.mockito.inline.tests">
- <application />
-</manifest>
diff --git a/dexmaker-mockito-inline-tests/CMakeLists.txt b/dexmaker-mockito-inline-tests/CMakeLists.txt
new file mode 100644
index 0000000..32ce07e
--- /dev/null
+++ b/dexmaker-mockito-inline-tests/CMakeLists.txt
@@ -0,0 +1,33 @@
+cmake_minimum_required(VERSION 3.4.1)
+
+set(slicer_sources
+ ../dexmaker-mockito-inline/external/slicer/bytecode_encoder.cc
+ ../dexmaker-mockito-inline/external/slicer/code_ir.cc
+ ../dexmaker-mockito-inline/external/slicer/common.cc
+ ../dexmaker-mockito-inline/external/slicer/control_flow_graph.cc
+ ../dexmaker-mockito-inline/external/slicer/debuginfo_encoder.cc
+ ../dexmaker-mockito-inline/external/slicer/dex_bytecode.cc
+ ../dexmaker-mockito-inline/external/slicer/dex_format.cc
+ ../dexmaker-mockito-inline/external/slicer/dex_ir_builder.cc
+ ../dexmaker-mockito-inline/external/slicer/dex_ir.cc
+ ../dexmaker-mockito-inline/external/slicer/dex_utf8.cc
+ ../dexmaker-mockito-inline/external/slicer/instrumentation.cc
+ ../dexmaker-mockito-inline/external/slicer/reader.cc
+ ../dexmaker-mockito-inline/external/slicer/tryblocks_encoder.cc
+ ../dexmaker-mockito-inline/external/slicer/writer.cc)
+
+add_library(slicer
+ STATIC
+ ${slicer_sources})
+
+include_directories(../dexmaker-mockito-inline/external/jdk ../dexmaker-mockito-inline/external/slicer/export/)
+
+target_link_libraries(slicer z)
+
+add_library(multiplejvmtiagentsinterferenceagent
+ SHARED
+ src/main/jni/multiplejvmtiagentsinterferenceagent/agent.cc)
+
+set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DANDROID_STL=c++_shared -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(multiplejvmtiagentsinterferenceagent slicer)
diff --git a/dexmaker-mockito-inline-tests/build.gradle b/dexmaker-mockito-inline-tests/build.gradle
new file mode 100644
index 0000000..5c86f89
--- /dev/null
+++ b/dexmaker-mockito-inline-tests/build.gradle
@@ -0,0 +1,53 @@
+buildscript {
+ repositories {
+ maven {
+ url "https://plugins.gradle.org/m2/"
+ }
+ }
+ dependencies {
+ classpath "net.ltgt.gradle:gradle-errorprone-plugin:0.0.13"
+ }
+}
+
+apply plugin: "net.ltgt.errorprone"
+apply plugin: 'com.android.library'
+
+android {
+ compileSdkVersion 28
+
+ android {
+ lintOptions {
+ disable 'InvalidPackage'
+ warning 'NewApi'
+ }
+ }
+
+ defaultConfig {
+ minSdkVersion 28
+ targetSdkVersion 28
+ versionName VERSION_NAME
+
+ testInstrumentationRunner 'android.support.test.runner.AndroidJUnitRunner'
+ }
+
+ externalNativeBuild {
+ cmake {
+ path 'CMakeLists.txt'
+ }
+ }
+}
+
+repositories {
+ jcenter()
+ google()
+}
+
+dependencies {
+ implementation project(':dexmaker-mockito-tests')
+ compileOnly project(':dexmaker-mockito-inline')
+ androidTestImplementation project(':dexmaker-mockito-inline')
+
+ implementation 'junit:junit:4.12'
+ implementation 'com.android.support.test:runner:1.0.1'
+ api 'org.mockito:mockito-core:2.19.0', { exclude group: 'net.bytebuddy' }
+}
diff --git a/dexmaker-mockito-inline-tests/src/androidTest/java/com/android/dx/mockito/tests b/dexmaker-mockito-inline-tests/src/androidTest/java/com/android/dx/mockito/tests
deleted file mode 120000
index f13e9f0..0000000
--- a/dexmaker-mockito-inline-tests/src/androidTest/java/com/android/dx/mockito/tests
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../../dexmaker-mockito-tests/src/androidTest/java/com/android/dx/mockito/tests
\ No newline at end of file
diff --git a/dexmaker-mockito-inline-tests/src/main/AndroidManifest.xml b/dexmaker-mockito-inline-tests/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..9f14dbf
--- /dev/null
+++ b/dexmaker-mockito-inline-tests/src/main/AndroidManifest.xml
@@ -0,0 +1,6 @@
+<manifest xmlns:tools="http://schemas.android.com/tools"
+ package="com.android.dexmaker.mockito.inline.tests"
+ xmlns:android="http://schemas.android.com/apk/res/android">
+ <application android:debuggable="true"
+ tools:ignore="HardcodedDebugMode" />
+</manifest>
diff --git a/dexmaker-mockito-inline-tests/src/androidTest/java/com/android/dx/mockito/inline/tests/MockFinal.java b/dexmaker-mockito-inline-tests/src/main/java/com/android/dx/mockito/inline/tests/MockFinal.java
similarity index 98%
rename from dexmaker-mockito-inline-tests/src/androidTest/java/com/android/dx/mockito/inline/tests/MockFinal.java
rename to dexmaker-mockito-inline-tests/src/main/java/com/android/dx/mockito/inline/tests/MockFinal.java
index fa02471..5903d45 100644
--- a/dexmaker-mockito-inline-tests/src/androidTest/java/com/android/dx/mockito/inline/tests/MockFinal.java
+++ b/dexmaker-mockito-inline-tests/src/main/java/com/android/dx/mockito/inline/tests/MockFinal.java
@@ -73,7 +73,7 @@
assertSame(fakeBinder, mockService.onBind(new Intent()));
}
- private final class FinalNonDefaultConstructorClass {
+ private static final class FinalNonDefaultConstructorClass {
public FinalNonDefaultConstructorClass(int i) {
}
@@ -121,6 +121,7 @@
}
private static final class SubClass extends SuperClass {
+ @Override
String returnC() {
return "subC";
}
diff --git a/dexmaker-mockito-inline-tests/src/androidTest/java/com/android/dx/mockito/inline/tests/MockNonPublic.java b/dexmaker-mockito-inline-tests/src/main/java/com/android/dx/mockito/inline/tests/MockNonPublic.java
similarity index 97%
rename from dexmaker-mockito-inline-tests/src/androidTest/java/com/android/dx/mockito/inline/tests/MockNonPublic.java
rename to dexmaker-mockito-inline-tests/src/main/java/com/android/dx/mockito/inline/tests/MockNonPublic.java
index aa828e5..60b2845 100644
--- a/dexmaker-mockito-inline-tests/src/androidTest/java/com/android/dx/mockito/inline/tests/MockNonPublic.java
+++ b/dexmaker-mockito-inline-tests/src/main/java/com/android/dx/mockito/inline/tests/MockNonPublic.java
@@ -108,6 +108,7 @@
}
private static class PrivateClass implements SingleMethodInterface {
+ @Override
public String returnA() {
return "A";
}
@@ -129,6 +130,7 @@
}
private interface PrivateInterface extends SingleMethodInterface {
+ @Override
String returnA();
}
@@ -138,6 +140,7 @@
}
private static class SubOfPrivateInterface implements PrivateInterface {
+ @Override
public String returnA() {
return "A";
}
@@ -159,10 +162,12 @@
}
private static abstract class PrivateAbstractClass implements DualMethodInterface {
+ @Override
public String returnA() {
return "A";
}
+ @Override
public abstract String returnB();
}
@@ -172,6 +177,7 @@
}
private static class SubOfPrivateAbstractClass extends PrivateAbstractClass {
+ @Override
public String returnB() {
return "B";
}
@@ -193,6 +199,7 @@
}
static class PackagePrivateClass implements SingleMethodInterface {
+ @Override
public String returnA() {
return "A";
}
@@ -204,10 +211,12 @@
}
static abstract class PackagePrivateAbstractClass implements DualMethodInterface {
+ @Override
public String returnA() {
return "A";
}
+ @Override
public abstract String returnB();
}
@@ -217,6 +226,7 @@
}
static class SubOfPackagePrivateAbstractClass extends PackagePrivateAbstractClass {
+ @Override
public String returnB() {
return "B";
}
@@ -238,6 +248,7 @@
}
interface PackagePrivateInterface extends SingleMethodInterface {
+ @Override
String returnA();
}
@@ -247,6 +258,7 @@
}
static class SubOfPackagePrivateInterface implements PackagePrivateInterface {
+ @Override
public String returnA() {
return "A";
}
@@ -331,6 +343,7 @@
public static class SubOfAbstractClassWithPackagePrivateMethod extends
AbstractClassWithPackagePrivateMethod {
+ @Override
String returnB() {
return "B";
}
diff --git a/dexmaker-mockito-inline-tests/src/androidTest/java/com/android/dx/mockito/inline/tests/MultipleJvmtiAgentsInterference.java b/dexmaker-mockito-inline-tests/src/main/java/com/android/dx/mockito/inline/tests/MultipleJvmtiAgentsInterference.java
similarity index 93%
rename from dexmaker-mockito-inline-tests/src/androidTest/java/com/android/dx/mockito/inline/tests/MultipleJvmtiAgentsInterference.java
rename to dexmaker-mockito-inline-tests/src/main/java/com/android/dx/mockito/inline/tests/MultipleJvmtiAgentsInterference.java
index 4f84276..bfc12fb 100644
--- a/dexmaker-mockito-inline-tests/src/androidTest/java/com/android/dx/mockito/inline/tests/MultipleJvmtiAgentsInterference.java
+++ b/dexmaker-mockito-inline-tests/src/main/java/com/android/dx/mockito/inline/tests/MultipleJvmtiAgentsInterference.java
@@ -16,7 +16,6 @@
package com.android.dx.mockito.inline.tests;
-import android.os.Build;
import android.os.Debug;
import org.junit.AfterClass;
@@ -24,13 +23,12 @@
import org.junit.Test;
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 = "libmultiplejvmtiagentsinterferenceagent.so";
- public class TestClass {
+ public static class TestClass {
public String returnA() {
return "A";
}
@@ -38,8 +36,6 @@
@BeforeClass
public static void installTestAgent() throws Exception {
- assumeTrue(Build.VERSION.SDK_INT >= 28);
-
Debug.attachJvmtiAgent(AGENT_LIB_NAME, null,
MultipleJvmtiAgentsInterference.class.getClassLoader());
}
diff --git a/dexmaker-mockito-inline/CMakeLists.txt b/dexmaker-mockito-inline/CMakeLists.txt
index b700cd4..1c0fa81 100644
--- a/dexmaker-mockito-inline/CMakeLists.txt
+++ b/dexmaker-mockito-inline/CMakeLists.txt
@@ -28,6 +28,6 @@
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")
+set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DANDROID_STL=c++_shared -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
index 853f061..96ad295 100644
--- a/dexmaker-mockito-inline/build.gradle
+++ b/dexmaker-mockito-inline/build.gradle
@@ -1,16 +1,36 @@
+buildscript {
+ repositories {
+ maven {
+ url "https://plugins.gradle.org/m2/"
+ }
+ }
+ dependencies {
+ classpath "net.ltgt.gradle:gradle-errorprone-plugin:0.0.13"
+ }
+}
+
+apply plugin: "net.ltgt.errorprone"
apply plugin: 'com.android.library'
+apply plugin: 'maven-publish'
+apply plugin: 'ivy-publish'
+apply plugin: 'com.jfrog.artifactory'
+
+version = VERSION_NAME
android {
- compileSdkVersion 'android-P'
- buildToolsVersion "25.0.0"
+ compileSdkVersion 28
+ buildToolsVersion '28.0.0'
- lintOptions {
- abortOnError false
+ android {
+ lintOptions {
+ disable 'InvalidPackage'
+ warning 'NewApi'
+ }
}
defaultConfig {
- minSdkVersion 25
- targetSdkVersion 25
+ minSdkVersion 1
+ targetSdkVersion 28
versionName VERSION_NAME
}
@@ -19,15 +39,80 @@
path 'CMakeLists.txt'
}
}
+}
+tasks.withType(JavaCompile) {
+ options.compilerArgs += ["-Xep:StringSplitter:OFF"]
+}
+
+task sourcesJar(type: Jar) {
+ classifier = 'sources'
+ from android.sourceSets.main.java.srcDirs
+}
+
+task javadoc(type: Javadoc) {
+ source = android.sourceSets.main.java.srcDirs
+ classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
+}
+
+task javadocJar(type: Jar, dependsOn: javadoc) {
+ classifier = 'javadoc'
+ from javadoc.destinationDir
+}
+
+publishing {
+ publications {
+ ivyLib(IvyPublication) {
+ from new org.gradle.api.internal.java.JavaLibrary(new org.gradle.api.internal.artifacts.publish.DefaultPublishArtifact(project.getName(), 'aar', 'aar', null, new Date(), new File("$buildDir/outputs/aar/${project.getName()}-release.aar"), assemble), project.configurations.implementation.getAllDependencies())
+ artifact sourcesJar
+ artifact javadocJar
+ }
+
+ lib(MavenPublication) {
+ from new org.gradle.api.internal.java.JavaLibrary(new org.gradle.api.internal.artifacts.publish.DefaultPublishArtifact(project.getName(), 'aar', 'aar', null, new Date(), new File("$buildDir/outputs/aar/${project.getName()}-release.aar"), assemble), project.configurations.implementation.getAllDependencies())
+
+ artifact sourcesJar
+ artifact javadocJar
+
+ pom.withXml {
+ asNode().children().last() + {
+ resolveStrategy = Closure.DELEGATE_FIRST
+ description = 'Implementation of the Mockito Inline API for use on the Android Dalvik VM'
+ url 'https://github.com/linkedin/dexmaker'
+ scm {
+ url 'https://github.com/linkedin/dexmaker'
+ connection 'scm:git:git://github.com/linkedin/dexmaker.git'
+ developerConnection 'https://github.com/linkedin/dexmaker.git'
+ }
+ licenses {
+ license {
+ name 'The Apache Software License, Version 2.0'
+ url 'http://www.apache.org/license/LICENSE-2.0.txt'
+ distribution 'repo'
+ }
+ }
+
+ developers {
+ developer {
+ id 'com.linkedin'
+ name 'LinkedIn Corp'
+ email ''
+ }
+ }
+ }
+ }
+ }
+ }
}
repositories {
jcenter()
+ google()
}
dependencies {
- compile project(':dexmaker')
- compile 'org.mockito:mockito-core:2.15.0', { exclude group: "net.bytebuddy" }
+ implementation project(':dexmaker')
+
+ implementation 'org.mockito:mockito-core:2.19.0', { exclude group: 'net.bytebuddy' }
}
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
index c702b2f..89f713a 100644
--- 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
@@ -26,7 +26,6 @@
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
-import java.util.Random;
import java.util.Set;
/**
@@ -59,7 +58,6 @@
Float.class,
Double.class,
String.class));
- private final static Random random = new Random();
/** Jvmti agent responsible for triggering transformation s*/
private final JvmtiAgent agent;
@@ -99,7 +97,7 @@
Map<Object, InvocationHandlerAdapter> mocks) {
this.agent = agent;
mockedTypes = Collections.synchronizedSet(new HashSet<Class<?>>());
- identifier = Long.toString(random.nextLong());
+ identifier = String.valueOf(System.identityHashCode(this));
MockMethodAdvice advice = new MockMethodAdvice(mocks);
try {
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
index 883f77b..5cb3817 100644
--- 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
@@ -40,7 +40,8 @@
&& !(className.startsWith("com.android.dx.mockito.")
// Do not clean unit tests
&& !className.startsWith("com.android.dx.mockito.tests")
- && !className.startsWith("com.android.dx.mockito.inline.tests"))
+ && !className.startsWith("com.android.dx.mockito.inline.tests")
+ && !className.startsWith("com.android.dx.mockito.inline.extended.tests"))
// dalvik interface proxies
&& !className.startsWith("$Proxy")
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
index abcf4f5..fc2a641 100644
--- 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
@@ -24,12 +24,12 @@
import com.android.dx.stock.ProxyBuilder.MethodSetEntry;
import org.mockito.Mockito;
+import org.mockito.creation.instance.Instantiator;
import org.mockito.exceptions.base.MockitoException;
-import org.mockito.internal.creation.instance.Instantiator;
import org.mockito.internal.util.reflection.LenientCopyTool;
import org.mockito.invocation.MockHandler;
import org.mockito.mock.MockCreationSettings;
-import org.mockito.plugins.InstantiatorProvider;
+import org.mockito.plugins.InstantiatorProvider2;
import org.mockito.plugins.MockMaker;
import java.io.IOException;
@@ -53,6 +53,7 @@
*
* <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";
@@ -68,7 +69,13 @@
* Class injected into the bootstrap classloader. All entry hooks added to methods will call
* this class.
*/
- private static final Class DISPATCHER_CLASS;
+ public static final Class DISPATCHER_CLASS;
+
+ /**
+ * {@code ExtendedMockito#spyOn} allows to turn an existing object into a spy. If this operation
+ * is running this field is set to the object that should become a spy.
+ */
+ public static ThreadLocal<Object> onSpyInProgressInstance = new ThreadLocal<>();
/*
* One time setup to allow the system to mocking via this mock maker.
@@ -260,14 +267,20 @@
Class<? extends T> proxyClass;
Instantiator instantiator = Mockito.framework().getPlugins()
- .getDefaultPlugin(InstantiatorProvider.class).getInstantiator(settings);
+ .getDefaultPlugin(InstantiatorProvider2.class).getInstantiator(settings);
if (subclassingRequired) {
try {
// support abstract methods via dexmaker's ProxyBuilder
- proxyClass = ProxyBuilder.forClass(typeToMock).implementing(extraInterfaces)
- .onlyMethods(getMethodsToProxy(settings)).withSharedClassLoader()
- .buildProxyClass();
+ ProxyBuilder builder = ProxyBuilder.forClass(typeToMock).implementing
+ (extraInterfaces)
+ .onlyMethods(getMethodsToProxy(settings)).withSharedClassLoader();
+
+ if (Build.VERSION.SDK_INT >= 28) {
+ builder.markTrusted();
+ }
+
+ proxyClass = builder.buildProxyClass();
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
@@ -276,18 +289,23 @@
try {
mock = instantiator.newInstance(proxyClass);
- } catch (org.mockito.internal.creation.instance.InstantiationException e) {
+ } catch (org.mockito.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);
+ if (settings.getSpiedInstance() != null
+ && onSpyInProgressInstance.get() == settings.getSpiedInstance()) {
+ mock = (T) onSpyInProgressInstance.get();
+ } else {
+ try {
+ mock = instantiator.newInstance(typeToMock);
+ } catch (org.mockito.creation.instance.InstantiationException e) {
+ throw new MockitoException("Unable to create mock instance of type '"
+ + typeToMock.getSimpleName() + "'", e);
+ }
}
}
}
@@ -358,7 +376,7 @@
private static final int MAX_GET_WITHOUT_CLEAN = 16384;
private final Object lock = new Object();
- private static StrongKey cachedKey;
+ private StrongKey cachedKey;
private HashMap<WeakKey, InvocationHandlerAdapter> adapters = new HashMap<>();
@@ -424,6 +442,7 @@
return adapters.isEmpty();
}
+ @SuppressWarnings("CollectionIncompatibleType")
@Override
public boolean containsKey(Object mock) {
synchronized (lock) {
@@ -442,6 +461,7 @@
}
}
+ @SuppressWarnings("CollectionIncompatibleType")
@Override
public InvocationHandlerAdapter get(Object mock) {
synchronized (lock) {
@@ -504,6 +524,7 @@
}
}
+ @SuppressWarnings("CollectionIncompatibleType")
@Override
public InvocationHandlerAdapter remove(Object mock) {
synchronized (lock) {
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
index 34c5c48..84d22e3 100644
--- 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
@@ -51,8 +51,9 @@
* @throws IOException If jvmti could not be enabled or agent could not be loaded
*/
JvmtiAgent() throws IOException {
- if (Build.VERSION.SDK_INT < 28) {
- throw new IOException("Requires API 28. API is " + Build.VERSION.SDK_INT);
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
+ throw new IOException("Requires API level " + Build.VERSION_CODES.P + ". API level is "
+ + Build.VERSION.SDK_INT);
}
ClassLoader cl = JvmtiAgent.class.getClassLoader();
diff --git a/dexmaker-mockito-inline/src/main/java/com/android/dx/mockito/inline/MockMakerMultiplexer.java b/dexmaker-mockito-inline/src/main/java/com/android/dx/mockito/inline/MockMakerMultiplexer.java
new file mode 100644
index 0000000..c5f8db6
--- /dev/null
+++ b/dexmaker-mockito-inline/src/main/java/com/android/dx/mockito/inline/MockMakerMultiplexer.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2018 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.util.Log;
+
+import org.mockito.invocation.MockHandler;
+import org.mockito.mock.MockCreationSettings;
+import org.mockito.plugins.MockMaker;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+
+/**
+ * Multiplexes multiple mock makers
+ */
+public final class MockMakerMultiplexer implements MockMaker {
+ private static final String LOG_TAG = MockMakerMultiplexer.class.getSimpleName();
+ private final static MockMaker[] MOCK_MAKERS;
+
+ static {
+ String[] potentialMockMakers = new String[] {
+ "com.android.dx.mockito.inline.InlineStaticMockMaker",
+ InlineDexmakerMockMaker.class.getName()
+ };
+
+ ArrayList<MockMaker> mockMakers = new ArrayList<>();
+ for (String potentialMockMaker : potentialMockMakers) {
+ try {
+ Class<? extends MockMaker> mockMakerClass = (Class<? extends MockMaker>)
+ Class.forName(potentialMockMaker);
+ mockMakers.add(mockMakerClass.getDeclaredConstructor().newInstance());
+ } catch (ClassNotFoundException | InstantiationException | IllegalAccessException
+ | NoSuchMethodException | InvocationTargetException e) {
+ if (potentialMockMaker.equals(InlineDexmakerMockMaker.class.getName())) {
+ Log.e(LOG_TAG, "Could not init mockmaker " + potentialMockMaker, e);
+ } else {
+ // Additional mock makers might not be loaded
+ Log.e(LOG_TAG, "Could not init mockmaker " + potentialMockMaker);
+ }
+ }
+ }
+
+ MOCK_MAKERS = mockMakers.toArray(new MockMaker[]{});
+ }
+
+ @Override
+ public <T> T createMock(MockCreationSettings<T> settings, MockHandler handler) {
+ for (MockMaker mockMaker : MOCK_MAKERS) {
+ T mock = mockMaker.createMock(settings, handler);
+
+ if (mock != null) {
+ return mock;
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ public MockHandler getHandler(Object mock) {
+ for (MockMaker mockMaker : MOCK_MAKERS) {
+ MockHandler handler = mockMaker.getHandler(mock);
+
+ if (handler != null) {
+ return handler;
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ public void resetMock(Object mock, MockHandler newHandler, MockCreationSettings settings) {
+ for (MockMaker mockMaker : MOCK_MAKERS) {
+ mockMaker.resetMock(mock, newHandler, settings);
+ }
+ }
+
+ @Override
+ public TypeMockability isTypeMockable(Class<?> type) {
+ for (MockMaker mockMaker : MOCK_MAKERS) {
+ TypeMockability mockability = mockMaker.isTypeMockable(type);
+
+ if (mockability != null) {
+ return mockability;
+ }
+ }
+
+ return null;
+ }
+}
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
index dfe242f..74e24b0 100644
--- 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
@@ -5,6 +5,7 @@
package com.android.dx.mockito.inline;
+import java.lang.ref.WeakReference;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
@@ -24,6 +25,7 @@
/** Pattern to decompose a instrumentedMethodWithTypeAndSignature */
private final Pattern methodPattern = Pattern.compile("(.*)#(.*)\\((.*)\\)");
+ @SuppressWarnings("ThreadLocalUsage")
private final SelfCallInfo selfCallInfo = new SelfCallInfo();
MockMethodAdvice(Map<Object, InvocationHandlerAdapter> interceptors) {
@@ -255,14 +257,14 @@
private static class SuperMethodCall implements InvocationHandlerAdapter.SuperMethod {
private final SelfCallInfo selfCallInfo;
private final Method origin;
- private final Object instance;
+ private final WeakReference<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.instance = new WeakReference<>(instance);
this.arguments = arguments;
}
@@ -281,8 +283,8 @@
// 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);
+ selfCallInfo.set(instance.get());
+ return tryInvoke(origin, instance.get(), arguments);
}
}
diff --git a/dexmaker-mockito-inline/src/main/resources/README.txt b/dexmaker-mockito-inline/src/main/resources/README.txt
index b94264b..fcf183f 100644
--- a/dexmaker-mockito-inline/src/main/resources/README.txt
+++ b/dexmaker-mockito-inline/src/main/resources/README.txt
@@ -1,2 +1,7 @@
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
+repackaged into a jar. We should automate this.
+
+unzip dexmaker-mockito-inline-dispatcher/build/outputs/apk/release/dexmaker-mockito-inline
+-dispatcher-release-unsigned.apk classes.dex
+jar -cf dexmaker-mockito-inline/src/main/resources/dispatcher.jar classes.dex
+rm classes.dex
diff --git a/dexmaker-mockito-inline/src/main/resources/dispatcher.jar b/dexmaker-mockito-inline/src/main/resources/dispatcher.jar
index 6a5cf5f..762c153 100644
--- a/dexmaker-mockito-inline/src/main/resources/dispatcher.jar
+++ 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
index 4f2b284..15d3297 100644
--- 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
@@ -1 +1 @@
-com.android.dx.mockito.inline.InlineDexmakerMockMaker
\ No newline at end of file
+com.android.dx.mockito.inline.MockMakerMultiplexer
diff --git a/dexmaker-mockito-tests/AndroidManifest.xml b/dexmaker-mockito-tests/AndroidManifest.xml
deleted file mode 100644
index 45201f9..0000000
--- a/dexmaker-mockito-tests/AndroidManifest.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-<manifest package="com.android.dexmaker.mockito.tests">
- <application />
-</manifest>
diff --git a/dexmaker-mockito-tests/build.gradle b/dexmaker-mockito-tests/build.gradle
index a08e254..70f3dfa 100644
--- a/dexmaker-mockito-tests/build.gradle
+++ b/dexmaker-mockito-tests/build.gradle
@@ -1,20 +1,33 @@
-apply plugin: 'com.android.application'
+buildscript {
+ repositories {
+ maven {
+ url "https://plugins.gradle.org/m2/"
+ }
+ }
+ dependencies {
+ classpath "net.ltgt.gradle:gradle-errorprone-plugin:0.0.13"
+ }
+}
+
+apply plugin: "net.ltgt.errorprone"
+apply plugin: 'com.android.library'
android {
- compileSdkVersion 25
- buildToolsVersion "25.0.0"
+ compileSdkVersion 28
- lintOptions {
- abortOnError false
+ android {
+ lintOptions {
+ disable 'InvalidPackage'
+ disable 'NewApi'
+ }
}
defaultConfig {
- applicationId "com.android.dexmaker.mockito.tests"
- minSdkVersion 25
- targetSdkVersion 25
+ minSdkVersion 8
+ targetSdkVersion 28
versionName VERSION_NAME
- testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+ testInstrumentationRunner 'android.support.test.runner.AndroidJUnitRunner'
}
}
@@ -24,9 +37,10 @@
}
dependencies {
- compile project(':dexmaker')
- compile project(':dexmaker-mockito')
+ compileOnly project(':dexmaker-mockito')
+ androidTestImplementation project(':dexmaker-mockito')
- androidTestCompile 'com.android.support.test:runner:0.5'
- androidTestCompile 'junit:junit:4.12'
+ implementation 'com.android.support.test:runner:0.5'
+ implementation 'junit:junit:4.12'
+ api 'org.mockito:mockito-core:2.19.0', { exclude group: 'net.bytebuddy' }
}
diff --git a/dexmaker-mockito-tests/src/main/AndroidManifest.xml b/dexmaker-mockito-tests/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..2f3d475
--- /dev/null
+++ b/dexmaker-mockito-tests/src/main/AndroidManifest.xml
@@ -0,0 +1,7 @@
+<manifest xmlns:tools="http://schemas.android.com/tools"
+ package="com.android.dexmaker.mockito.tests"
+ xmlns:android="http://schemas.android.com/apk/res/android">
+ <application android:debuggable="true"
+ tools:ignore="HardcodedDebugMode"/>
+</manifest>
+
diff --git a/dexmaker-mockito-tests/src/androidTest/java/com/android/dx/mockito/tests/BlacklistedApis.java b/dexmaker-mockito-tests/src/main/java/com/android/dx/mockito/tests/BlacklistedApis.java
similarity index 90%
rename from dexmaker-mockito-tests/src/androidTest/java/com/android/dx/mockito/tests/BlacklistedApis.java
rename to dexmaker-mockito-tests/src/main/java/com/android/dx/mockito/tests/BlacklistedApis.java
index ffe55fb..2144c50 100644
--- a/dexmaker-mockito-tests/src/androidTest/java/com/android/dx/mockito/tests/BlacklistedApis.java
+++ b/dexmaker-mockito-tests/src/main/java/com/android/dx/mockito/tests/BlacklistedApis.java
@@ -23,16 +23,19 @@
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
+import android.annotation.SuppressLint;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
+import android.os.Build;
import android.provider.Settings;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import android.view.View;
import android.widget.FrameLayout;
+import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -41,6 +44,11 @@
*/
@RunWith(AndroidJUnit4.class)
public class BlacklistedApis {
+ @Before
+ public void onlyRunOnPlatformsThatSupportBlacklisting() {
+ assumeTrue(Build.VERSION.SDK_INT >= 28);
+ }
+
/**
* Check if the application is marked as {@code android:debuggable} in the manifest
*
@@ -84,6 +92,7 @@
@Test
public void copyBlacklistedFields() throws Exception {
+ // Can only copy blacklisted fields when debuggable
assumeTrue(isDebuggable());
Context targetContext = InstrumentationRegistry.getTargetContext();
@@ -105,6 +114,7 @@
parent.measure(100, 100);
}
+ @SuppressLint({"PrivateApi", "CheckReturnValue"})
@Test
public void cannotCallBlackListedAfterSpying() {
// Spying and mocking might change the View class's byte code
@@ -120,8 +130,9 @@
}
}
- public class CallBlackListedMethod {
- public boolean callingBlacklistedMethodCausesException() {
+ public static class CallBlackListedMethod {
+ @SuppressLint("PrivateApi")
+ boolean callingBlacklistedMethodCausesException() {
// Settings.Global#isValidZenMode is a blacklisted method. Resolving it should fail
try {
Settings.Global.class.getDeclaredMethod("isValidZenMode", Integer.TYPE);
@@ -138,7 +149,8 @@
assertTrue(t.callingBlacklistedMethodCausesException());
}
- public abstract class CallBlacklistedMethodAbstract {
+ public static abstract class CallBlacklistedMethodAbstract {
+ @SuppressLint("PrivateApi")
public boolean callingBlacklistedMethodCausesException() {
// Settings.Global#isValidZenMode is a blacklisted method. Resolving it should fail
try {
diff --git a/dexmaker-mockito-tests/src/androidTest/java/com/android/dx/mockito/tests/CleanStackTrace.java b/dexmaker-mockito-tests/src/main/java/com/android/dx/mockito/tests/CleanStackTrace.java
similarity index 100%
rename from dexmaker-mockito-tests/src/androidTest/java/com/android/dx/mockito/tests/CleanStackTrace.java
rename to dexmaker-mockito-tests/src/main/java/com/android/dx/mockito/tests/CleanStackTrace.java
diff --git a/dexmaker-mockito-tests/src/androidTest/java/com/android/dx/mockito/tests/GeneralMocking.java b/dexmaker-mockito-tests/src/main/java/com/android/dx/mockito/tests/GeneralMocking.java
similarity index 100%
rename from dexmaker-mockito-tests/src/androidTest/java/com/android/dx/mockito/tests/GeneralMocking.java
rename to dexmaker-mockito-tests/src/main/java/com/android/dx/mockito/tests/GeneralMocking.java
diff --git a/dexmaker-mockito-tests/src/main/java/com/android/dx/mockito/tests/PartialClasses.java b/dexmaker-mockito-tests/src/main/java/com/android/dx/mockito/tests/PartialClasses.java
new file mode 100644
index 0000000..be9d790
--- /dev/null
+++ b/dexmaker-mockito-tests/src/main/java/com/android/dx/mockito/tests/PartialClasses.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2018 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 org.junit.Test;
+import org.mockito.exceptions.base.MockitoException;
+
+import java.util.AbstractList;
+
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.doCallRealMethod;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Tests what happens if code tries to call real methods of abstract classed and in interfaces
+ */
+public class PartialClasses {
+ @Test
+ public void callRealMethodOnInterface() {
+ Runnable r = mock(Runnable.class);
+
+ try {
+ doCallRealMethod().when(r).run();
+ fail();
+ } catch (MockitoException e) {
+ // expected
+ }
+ }
+
+ @Test
+ public void callAbstractRealMethodOnAbstractClass() {
+ AbstractList l = mock(AbstractList.class);
+
+ try {
+ when(l.size()).thenCallRealMethod();
+ fail();
+ } catch (MockitoException e) {
+ // expected
+ }
+ }
+
+ @Test
+ public void callRealMethodOnAbstractClass() {
+ AbstractList l = mock(AbstractList.class);
+
+ doCallRealMethod().when(l).clear();
+
+ l.clear();
+ }
+}
diff --git a/dexmaker-mockito-tests/src/main/java/com/android/dx/mockito/tests/Stress.java b/dexmaker-mockito-tests/src/main/java/com/android/dx/mockito/tests/Stress.java
new file mode 100644
index 0000000..ec4b8e8
--- /dev/null
+++ b/dexmaker-mockito-tests/src/main/java/com/android/dx/mockito/tests/Stress.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2018 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 android.util.Log;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@RunWith(AndroidJUnit4.class)
+public class Stress {
+ private static final String LOG_TAG = Stress.class.getSimpleName();
+ private static final int NUM_TESTS = 80000;
+
+ public static class TestClass {
+ public String echo(String in) {
+ return in;
+ }
+ }
+
+ @Test
+ public void mockALot() {
+ for (int i = 0; i < NUM_TESTS; i++) {
+ if (i % 1024 == 0) {
+ Log.i(LOG_TAG, "Ran " + i + "/" + NUM_TESTS + " tests");
+ }
+
+ TestClass m = mock(TestClass.class);
+ when(m.echo(eq("marco!"))).thenReturn("polo");
+ assertEquals("polo", m.echo("marco!"));
+ verify(m).echo("marco!");
+ }
+ }
+}
diff --git a/dexmaker-mockito/build.gradle b/dexmaker-mockito/build.gradle
index 96479c5..60cb77c 100644
--- a/dexmaker-mockito/build.gradle
+++ b/dexmaker-mockito/build.gradle
@@ -1,3 +1,15 @@
+buildscript {
+ repositories {
+ maven {
+ url "https://plugins.gradle.org/m2/"
+ }
+ }
+ dependencies {
+ classpath "net.ltgt.gradle:gradle-errorprone-plugin:0.0.13"
+ }
+}
+
+apply plugin: "net.ltgt.errorprone"
apply plugin: 'java'
apply from: "$rootDir/gradle/publishing.gradle"
@@ -12,7 +24,7 @@
}
dependencies {
- compile project(":dexmaker")
+ implementation project(':dexmaker')
- compile 'org.mockito:mockito-core:2.15.0', { exclude group: "net.bytebuddy" }
+ implementation 'org.mockito:mockito-core:2.19.0', { exclude group: 'net.bytebuddy' }
}
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 19f371e..4cdaf44 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
@@ -16,9 +16,6 @@
package com.android.dx.mockito;
-import android.os.Build;
-import android.util.Log;
-
import com.android.dx.stock.ProxyBuilder;
import org.mockito.exceptions.base.MockitoException;
import org.mockito.exceptions.stacktrace.StackTraceCleaner;
@@ -39,12 +36,19 @@
* Generates mock instances on Android's runtime.
*/
public final class DexmakerMockMaker implements MockMaker, StackTraceCleanerProvider {
- private static final String LOG_TAG = DexmakerMockMaker.class.getSimpleName();
-
private final UnsafeAllocator unsafeAllocator = UnsafeAllocator.create();
+ private boolean isApi28;
- public DexmakerMockMaker() {
- if (Build.VERSION.SDK_INT >= 28) {
+ public DexmakerMockMaker() throws Exception {
+ try {
+ Class buildVersion = Class.forName("android.os.Build$VERSION");
+ isApi28 = buildVersion.getDeclaredField("SDK_INT").getInt(null) >= 28;
+ } catch (ClassNotFoundException e) {
+ System.err.println("Could not determine platform API level, assuming >= 28: " + e);
+ isApi28 = true;
+ }
+
+ if (isApi28) {
// Blacklisted APIs were introduced in Android P:
//
// https://android-developers.googleblog.com/2018/02/
@@ -69,8 +73,8 @@
try {
allowHiddenApiReflectionFromMethod.invoke(null, LenientCopyTool.class);
} catch (InvocationTargetException | IllegalAccessException e) {
- Log.w(LOG_TAG, "Cannot allow LenientCopyTool to copy spies of blacklisted fields. "
- + "This might break spying on system classes.");
+ System.err.println("Cannot allow LenientCopyTool to copy spies of blacklisted "
+ + "fields. This might break spying on system classes.");
}
}
}
@@ -95,15 +99,19 @@
} else {
// support concrete classes via dexmaker's ProxyBuilder
try {
- ProxyBuilder b = ProxyBuilder.forClass(typeToMock)
+ ProxyBuilder builder = ProxyBuilder.forClass(typeToMock)
.implementing(extraInterfaces);
+ if (isApi28) {
+ builder.markTrusted();
+ }
+
if (Boolean.parseBoolean(
System.getProperty("dexmaker.share_classloader", "false"))) {
- b.withSharedClassLoader();
+ builder.withSharedClassLoader();
}
- Class<? extends T> proxyClass = b.buildProxyClass();
+ Class<? extends T> proxyClass = builder.buildProxyClass();
T mock = unsafeAllocator.newInstance(proxyClass);
ProxyBuilder.setInvocationHandler(mock, invocationHandler);
return mock;
diff --git a/dexmaker-tests/Android.mk b/dexmaker-tests/Android.mk
deleted file mode 100644
index 3ed4111..0000000
--- a/dexmaker-tests/Android.mk
+++ /dev/null
@@ -1,47 +0,0 @@
-# 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.
-#
-#
-
-LOCAL_PATH := $(call my-dir)
-
-# Build Dexmaker's tests
-#
-# Run the tests as follows:
-# vogar --classpath ${ANDROID_PRODUCT_OUT}/obj/JAVA_LIBRARIES/dexmaker-tests_intermediates/javalib.jar \
- com.android.dx
-
-include $(CLEAR_VARS)
-LOCAL_MODULE := dexmaker-tests
-LOCAL_SDK_VERSION := 17
-LOCAL_SRC_FILES := $(call all-java-files-under, src/androidTest/java)
-LOCAL_STATIC_JAVA_LIBRARIES := dexmaker android-support-test
-LOCAL_ERROR_PRONE_FLAGS := -Xep:JUnit4TestNotRun:WARN
-include $(BUILD_STATIC_JAVA_LIBRARY)
-
-# Build a test APK
-#
-# Run the tests as follows:
-# m -j32 DexmakerTests && \
- am install -r -g $OUT/data/app/DexmakerTests/DexmakerTests.apk \
- adb shell am instrument -w com.google.dexmaker.tests
-#
-include $(CLEAR_VARS)
-LOCAL_MODULE_TAGS := tests
-LOCAL_PACKAGE_NAME := DexmakerTests
-LOCAL_SDK_VERSION := current
-LOCAL_STATIC_JAVA_LIBRARIES := \
- dexmaker-tests
-
-include $(BUILD_PACKAGE)
diff --git a/dexmaker-tests/build.gradle b/dexmaker-tests/build.gradle
index 5bd40cd..6cf2958 100644
--- a/dexmaker-tests/build.gradle
+++ b/dexmaker-tests/build.gradle
@@ -1,27 +1,41 @@
-apply plugin: 'com.android.application'
-
-repositories {
- jcenter()
-}
-
-android {
- compileSdkVersion 25
- buildToolsVersion '25.0.0'
-
- defaultConfig {
- applicationId "com.linkedin.dexmaker"
- minSdkVersion 8
- targetSdkVersion 25
- versionCode 1
- versionName VERSION_NAME
-
- testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+buildscript {
+ repositories {
+ maven {
+ url "https://plugins.gradle.org/m2/"
+ }
+ }
+ dependencies {
+ classpath "net.ltgt.gradle:gradle-errorprone-plugin:0.0.13"
}
}
-dependencies {
- compile project(":dexmaker")
+apply plugin: "net.ltgt.errorprone"
+apply plugin: 'com.android.application'
- androidTestCompile 'com.android.support.test:runner:0.5'
- androidTestCompile 'junit:junit:4.12'
+android {
+ compileSdkVersion 28
+ buildToolsVersion '28.0.0'
+
+ defaultConfig {
+ applicationId 'com.linkedin.dexmaker'
+ minSdkVersion 8
+ targetSdkVersion 28
+ versionCode 1
+ versionName VERSION_NAME
+
+ testInstrumentationRunner 'android.support.test.runner.AndroidJUnitRunner'
+ }
+}
+
+repositories {
+ jcenter()
+ google()
+}
+
+dependencies {
+ implementation project(":dexmaker")
+
+ //noinspection GradleDependency
+ androidTestImplementation 'com.android.support.test:runner:0.5'
+ androidTestImplementation 'junit:junit:4.12'
}
diff --git a/dexmaker-tests/src/androidTest/java/com/android/dx/AnnotationIdTest.java b/dexmaker-tests/src/androidTest/java/com/android/dx/AnnotationIdTest.java
index 43731ee..4e36988 100644
--- a/dexmaker-tests/src/androidTest/java/com/android/dx/AnnotationIdTest.java
+++ b/dexmaker-tests/src/androidTest/java/com/android/dx/AnnotationIdTest.java
@@ -15,6 +15,7 @@
*/
package com.android.dx;
+import android.os.Build;
import android.support.test.InstrumentationRegistry;
import org.junit.After;
import org.junit.Before;
@@ -31,6 +32,7 @@
import static java.lang.reflect.Modifier.PUBLIC;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
public final class AnnotationIdTest {
@@ -227,6 +229,8 @@
*/
@Test
public void addMethodAnnotationWithEnumElement() throws Exception {
+ assumeTrue(Build.VERSION.SDK_INT >= 21);
+
MethodId<?, Void> methodId = generateVoidMethod(TypeId.get(Enum.class));
AnnotationId.Element element = new AnnotationId.Element("elementEnum", ElementEnum.INSTANCE_1);
addAnnotationToMethod(methodId, element);
@@ -259,6 +263,8 @@
*/
@Test
public void addMethodAnnotationWithMultiElements() throws Exception {
+ assumeTrue(Build.VERSION.SDK_INT >= 21);
+
MethodId<?, Void> methodId = generateVoidMethod();
AnnotationId.Element element1 = new AnnotationId.Element("elementClass", AnnotationId.class);
AnnotationId.Element element2 = new AnnotationId.Element("elementEnum", ElementEnum.INSTANCE_1);
@@ -280,6 +286,8 @@
*/
@Test
public void addMethodAnnotationWithDuplicateElements() throws Exception {
+ assumeTrue(Build.VERSION.SDK_INT >= 21);
+
MethodId<?, Void> methodId = generateVoidMethod();
AnnotationId.Element element1 = new AnnotationId.Element("elementEnum", ElementEnum.INSTANCE_1);
AnnotationId.Element element2 = new AnnotationId.Element("elementEnum", ElementEnum.INSTANCE_0);
diff --git a/dexmaker-tests/src/androidTest/java/com/android/dx/AppDataDirGuesserTest.java b/dexmaker-tests/src/androidTest/java/com/android/dx/AppDataDirGuesserTest.java
index b1ff25f..d979279 100644
--- a/dexmaker-tests/src/androidTest/java/com/android/dx/AppDataDirGuesserTest.java
+++ b/dexmaker-tests/src/androidTest/java/com/android/dx/AppDataDirGuesserTest.java
@@ -129,6 +129,7 @@
private TestCondition guessCacheDirFor(final String path) {
final Set<String> notWriteable = new HashSet<>();
return new TestCondition() {
+ @Override
public void shouldGive(String... files) {
AppDataDirGuesser guesser = new AppDataDirGuesser() {
@Override
@@ -148,6 +149,7 @@
}
}
+ @Override
public TestCondition withNonWriteable(String... files) {
notWriteable.addAll(Arrays.asList(files));
return this;
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 f2b457f..c83fcec 100644
--- a/dexmaker-tests/src/androidTest/java/com/android/dx/DexMakerTest.java
+++ b/dexmaker-tests/src/androidTest/java/com/android/dx/DexMakerTest.java
@@ -16,7 +16,9 @@
package com.android.dx;
+import android.os.Build;
import android.support.test.InstrumentationRegistry;
+
import org.junit.Before;
import org.junit.Test;
@@ -31,6 +33,8 @@
import java.util.List;
import java.util.concurrent.Callable;
+import dalvik.system.BaseDexClassLoader;
+
import static com.android.dx.util.TestUtil.DELTA_DOUBLE;
import static com.android.dx.util.TestUtil.DELTA_FLOAT;
import static java.lang.reflect.Modifier.ABSTRACT;
@@ -43,8 +47,12 @@
import static java.lang.reflect.Modifier.SYNCHRONIZED;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
/**
* This generates a class named 'Generated' with one or more generated methods
@@ -135,7 +143,7 @@
addDefaultConstructor();
Class<?> generatedClass = generateAndLoad();
- Object instance = generatedClass.newInstance();
+ Object instance = generatedClass.getDeclaredConstructor().newInstance();
Method method = generatedClass.getMethod("call");
method.invoke(instance);
}
@@ -177,7 +185,7 @@
addDefaultConstructor();
Class<?> generatedClass = generateAndLoad();
- Object instance = generatedClass.newInstance();
+ Object instance = generatedClass.getDeclaredConstructor().newInstance();
Method method = generatedClass.getMethod("call", int.class);
method.invoke(instance, 0);
}
@@ -244,7 +252,7 @@
addDefaultConstructor();
Class<?> generatedClass = generateAndLoad();
- Object instance = generatedClass.newInstance();
+ Object instance = generatedClass.getDeclaredConstructor().newInstance();
Method method = generatedClass.getMethod("call", generatedClass);
assertEquals(5, method.invoke(null, instance));
}
@@ -278,7 +286,7 @@
addDefaultConstructor();
Class<?> generatedClass = generateAndLoad();
- Object instance = generatedClass.newInstance();
+ Object instance = generatedClass.getDeclaredConstructor().newInstance();
Method method = generatedClass.getMethod("superHashCode");
assertEquals(System.identityHashCode(instance), method.invoke(instance));
}
@@ -299,6 +307,7 @@
code.returnValue(localResult);
Callable<Object> callable = new Callable<Object>() {
+ @Override
public Object call() throws Exception {
return "abc";
}
@@ -425,7 +434,7 @@
addDefaultConstructor();
Class<?> generatedClass = generateAndLoad();
- Object instance = generatedClass.newInstance();
+ Object instance = generatedClass.getDeclaredConstructor().newInstance();
Field a = generatedClass.getField("a");
assertEquals(int.class, a.getType());
@@ -691,6 +700,7 @@
}
@Test
+ @SuppressWarnings("FloatingPointLiteralPrecision")
public void testCastFloatingPointToInteger() throws Exception {
Method floatToInt = numericCastingMethod(float.class, int.class);
assertEquals(0, floatToInt.invoke(null, 0.0f));
@@ -1046,7 +1056,7 @@
assertEquals((short) 0x1234, instance.shortValue);
}
- public class Instance {
+ public static class Instance {
public int intValue;
public long longValue;
public float floatValue;
@@ -1871,7 +1881,7 @@
addDefaultConstructor();
Class<?> generatedClass = generateAndLoad();
- Object instance = generatedClass.newInstance();
+ Object instance = generatedClass.getDeclaredConstructor().newInstance();
Method method = generatedClass.getMethod("call");
assertTrue(Modifier.isSynchronized(method.getModifiers()));
try {
@@ -1905,7 +1915,7 @@
addDefaultConstructor();
Class<?> generatedClass = generateAndLoad();
- Object instance = generatedClass.newInstance();
+ Object instance = generatedClass.getDeclaredConstructor().newInstance();
Method method = generatedClass.getMethod("call");
assertFalse(Modifier.isSynchronized(method.getModifiers()));
method.invoke(instance); // will take 100ms
@@ -2121,7 +2131,7 @@
TypeId<IllegalStateException> iseType = TypeId.get(IllegalStateException.class);
Local<IllegalStateException> localIse = code.newLocal(iseType);
if (params.length > 0) {
- if (params[0] == typeId) {
+ if (params[0].equals(typeId)) {
Local<?> localResult = code.getParameter(0, TypeId.INT);
code.returnValue(localResult);
} else {
@@ -2188,6 +2198,7 @@
private File[] getJarFiles() {
return getDataDirectory().listFiles(new FilenameFilter() {
+ @Override
public boolean accept(File dir, String name) {
return name.endsWith(".jar");
}
@@ -2216,4 +2227,64 @@
return dexMaker.generateAndLoad(getClass().getClassLoader(), getDataDirectory())
.loadClass("Generated");
}
+
+ private final ClassLoader commonClassLoader = new BaseDexClassLoader(
+ getDataDirectory().getPath(), getDataDirectory(), getDataDirectory().getPath(),
+ DexMakerTest.class.getClassLoader());
+
+ private final ClassLoader uncommonClassLoader = new ClassLoader() {
+ @Override
+ public Class<?> loadClass(String name) throws ClassNotFoundException {
+ throw new IllegalStateException("Not used");
+ }
+ };
+
+ private static void loadWithSharedClassLoader(ClassLoader cl, boolean markAsTrusted,
+ boolean shouldUseCL) throws Exception {
+ DexMaker d = new DexMaker();
+ d.setSharedClassLoader(cl);
+
+ if (markAsTrusted) {
+ d.markAsTrusted();
+ }
+
+ ClassLoader selectedCL = d.generateAndLoad(null, getDataDirectory());
+
+ if (shouldUseCL) {
+ assertSame(cl, selectedCL);
+ } else {
+ assertNotSame(cl, selectedCL);
+
+ // An appropriate fallback should have been selected
+ assertNotNull(selectedCL);
+ }
+ }
+
+ @Test
+ public void loadWithUncommonSharedClassLoader() throws Exception{
+ assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N);
+
+ loadWithSharedClassLoader(uncommonClassLoader, false, false);
+ }
+
+ @Test
+ public void loadWithCommonSharedClassLoader() throws Exception{
+ assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N);
+
+ loadWithSharedClassLoader(commonClassLoader, false, true);
+ }
+
+ @Test
+ public void loadAsTrustedWithUncommonSharedClassLoader() throws Exception{
+ assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P);
+
+ loadWithSharedClassLoader(uncommonClassLoader, true, false);
+ }
+
+ @Test
+ public void loadAsTrustedWithCommonSharedClassLoader() throws Exception{
+ assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P);
+
+ loadWithSharedClassLoader(commonClassLoader, true, true);
+ }
}
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 7127ec5..4092390 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
@@ -16,6 +16,8 @@
package com.android.dx.stock;
+import android.os.Build;
+
import com.android.dx.DexMakerTest;
import junit.framework.AssertionFailedError;
import org.junit.After;
@@ -44,6 +46,7 @@
import static junit.framework.Assert.assertTrue;
import static junit.framework.Assert.fail;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assume.assumeTrue;
public class ProxyBuilderTest {
private FakeInvocationHandler fakeHandler = new FakeInvocationHandler();
@@ -202,6 +205,7 @@
}
}
+ @Test
public void testProxyingPackagePrivateMethods_NotIntercepted()
throws Throwable {
HasPackagePrivateMethod proxy = proxyFor(HasPackagePrivateMethod.class)
@@ -215,12 +219,28 @@
try {
proxy.result();
- fail();
} catch (AssertionFailedError expected) {
+ return;
+ }
+ fail();
+ }
+ public static class HasPackagePrivateMethodSharedClassLoader {
+ String result() {
+ throw new AssertionFailedError();
}
}
+ @Test
+ public void testProxyingPackagePrivateMethodsWithSharedClassLoader_AreIntercepted()
+ throws Throwable {
+ assumeTrue(Build.VERSION.SDK_INT >= 24);
+
+ assertEquals("fake result", proxyFor(HasPackagePrivateMethodSharedClassLoader.class)
+ .withSharedClassLoader().build().result());
+ }
+
+
public static class HasProtectedMethod {
protected String result() {
throw new AssertionFailedError();
@@ -271,7 +291,7 @@
}
@Test
- @SuppressWarnings("EqualsWithItself")
+ @SuppressWarnings({"EqualsWithItself", "SelfEquals"})
public void testObjectMethodsAreAlsoProxied() throws Throwable {
Object proxy = proxyFor(Object.class).build();
fakeHandler.setFakeResult("mystring");
@@ -335,6 +355,7 @@
}
public static class InvokeSuperHandler implements InvocationHandler {
+ @Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return ProxyBuilder.callSuper(proxy, method, args);
}
@@ -405,6 +426,7 @@
@Test
public void testSinglePrimitiveParameter() throws Throwable {
InvocationHandler handler = new InvocationHandler() {
+ @Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return "asdf" + ((Integer) args[0]).intValue();
}
@@ -486,6 +508,7 @@
@Test
public void testSometimesDelegateToSuper() throws Exception {
InvocationHandler delegatesOddValues = new InvocationHandler() {
+ @Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().equals("method")) {
int intValue = ((Integer) args[0]).intValue();
@@ -508,6 +531,7 @@
@Test
public void testCallSuperThrows() throws Exception {
InvocationHandler handler = new InvocationHandler() {
+ @Override
public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
return ProxyBuilder.callSuper(o, method, objects);
}
@@ -582,6 +606,21 @@
assertEquals("fake result", proxyFor(AbstractClass.class).build().getValue());
}
+ @Test
+ public void testCallAbstractSuperMethod() throws Exception {
+ AbstractClass a = proxyFor(AbstractClass.class).build();
+
+ // Setting the handler to null routes all calls to the real methods. In this case the real
+ // method is abstract and cannot be called
+ ProxyBuilder.setInvocationHandler(a, null);
+
+ try {
+ a.getValue();
+ fail();
+ } catch (AbstractMethodError expected) {
+ }
+ }
+
public static class CtorHasDeclaredException {
public CtorHasDeclaredException() throws IOException {
throw new IOException();
@@ -616,10 +655,11 @@
}
try {
proxyFor(CtorHasError.class).build();
- fail();
} catch (Error expected) {
assertEquals("my message again", expected.getMessage());
+ return;
}
+ fail();
}
@Test
@@ -771,6 +811,7 @@
@Test
public void testImplementInterfaceCallingThroughConcreteClass() throws Throwable {
InvocationHandler invocationHandler = new InvocationHandler() {
+ @Override
public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
assertEquals("a", ProxyBuilder.callSuper(o, method, objects));
return "b";
@@ -796,6 +837,7 @@
final AtomicInteger count = new AtomicInteger();
InvocationHandler invocationHandler = new InvocationHandler() {
+ @Override
public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
count.incrementAndGet();
return ProxyBuilder.callSuper(o, method, objects);
@@ -817,6 +859,7 @@
}
public static class ImplementsCallable implements Callable<String> {
+ @Override
public String call() throws Exception {
return "a";
}
@@ -831,6 +874,7 @@
@Test
public void testInterfacesSameNamesDifferentReturnTypes() throws Throwable {
InvocationHandler handler = new InvocationHandler() {
+ @Override
public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
if (method.getReturnType() == void.class) {
return null;
@@ -859,6 +903,23 @@
assertEquals(3, c.foo());
}
+
+ @Test
+ public void testCallInterfaceSuperMethod() throws Exception {
+ FooReturnsVoid f = (FooReturnsVoid)proxyFor(Object.class).implementing(FooReturnsVoid.class)
+ .build();
+
+ // Setting the handler to null routes all calls to the real methods. In this case the real
+ // method is a method of an interface and cannot be called
+ ProxyBuilder.setInvocationHandler(f, null);
+
+ try {
+ f.foo();
+ fail();
+ } catch (AbstractMethodError expected) {
+ }
+ }
+
@Test
public void testInterfacesSameNamesSameReturnType() throws Throwable {
Object o = proxyFor(Object.class)
@@ -974,6 +1035,7 @@
private static class FakeInvocationHandler implements InvocationHandler {
private Object fakeResult = "fake result";
+ @Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return fakeResult;
}
@@ -1077,6 +1139,7 @@
}
public static class ConcreteClassA implements FooReturnsInt {
+ @Override
// from FooReturnsInt
public int foo() {
return 1;
@@ -1089,6 +1152,7 @@
}
public static class ConcreteClassB implements FooReturnsInt {
+ @Override
// from FooReturnsInt
public int foo() {
return 0;
@@ -1150,6 +1214,7 @@
}
InvocationHandler handler = new InvokeSuperHandler() {
+ @Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().equals("returnA")) {
return "fake A";
diff --git a/dexmaker-tests/AndroidManifest.xml b/dexmaker-tests/src/main/AndroidManifest.xml
similarity index 94%
rename from dexmaker-tests/AndroidManifest.xml
rename to dexmaker-tests/src/main/AndroidManifest.xml
index 7849b0b..62cf305 100644
--- a/dexmaker-tests/AndroidManifest.xml
+++ b/dexmaker-tests/src/main/AndroidManifest.xml
@@ -19,5 +19,5 @@
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
- <application/>
+ <application android:allowBackup="false" />
</manifest>
\ No newline at end of file
diff --git a/dexmaker/build.gradle b/dexmaker/build.gradle
index 73c9344..396243b 100644
--- a/dexmaker/build.gradle
+++ b/dexmaker/build.gradle
@@ -1,3 +1,15 @@
+buildscript {
+ repositories {
+ maven {
+ url "https://plugins.gradle.org/m2/"
+ }
+ }
+ dependencies {
+ classpath "net.ltgt.gradle:gradle-errorprone-plugin:0.0.13"
+ }
+}
+
+apply plugin: "net.ltgt.errorprone"
apply plugin: 'java'
apply from: "$rootDir/gradle/publishing.gradle"
@@ -11,6 +23,10 @@
jcenter()
}
+tasks.withType(JavaCompile) {
+ options.compilerArgs += ["-Xep:StringSplitter:OFF"]
+}
+
dependencies {
- compile 'com.jakewharton.android.repackaged:dalvik-dx:7.1.0_r7'
+ implementation 'com.jakewharton.android.repackaged:dalvik-dx:7.1.0_r7'
}
diff --git a/dexmaker/src/main/java/com/android/dx/AnnotationId.java b/dexmaker/src/main/java/com/android/dx/AnnotationId.java
index bcc201b..366c904 100644
--- a/dexmaker/src/main/java/com/android/dx/AnnotationId.java
+++ b/dexmaker/src/main/java/com/android/dx/AnnotationId.java
@@ -130,7 +130,7 @@
throw new IllegalStateException("This annotation is not for method");
}
- if (method.declaringType != declaringType) {
+ if (!method.declaringType.equals(declaringType)) {
throw new IllegalArgumentException("Method" + method + "'s declaring type is inconsistent with" + this);
}
diff --git a/dexmaker/src/main/java/com/android/dx/DexMaker.java b/dexmaker/src/main/java/com/android/dx/DexMaker.java
index 755c9fa..09d0f72 100644
--- a/dexmaker/src/main/java/com/android/dx/DexMaker.java
+++ b/dexmaker/src/main/java/com/android/dx/DexMaker.java
@@ -49,8 +49,6 @@
import static java.lang.reflect.Modifier.PRIVATE;
import static java.lang.reflect.Modifier.STATIC;
-import android.util.Log;
-
/**
* Generates a <strong>D</strong>alvik <strong>EX</strong>ecutable (dex)
* file for execution on Android. Dex files define classes and interfaces,
@@ -198,9 +196,13 @@
* }</pre>
*/
public final class DexMaker {
- private static final String LOG_TAG = DexMaker.class.getSimpleName();
-
private final Map<TypeId<?>, TypeDeclaration> types = new LinkedHashMap<>();
+
+ // Only warn about not being able to deal with blacklisted methods once. Often this is no
+ // problem and warning on every class load is too spammy.
+ private static boolean didWarnBlacklistedMethods;
+ private static boolean didWarnNonBaseDexClassLoader;
+
private ClassLoader sharedClassLoader;
private DexFile outputDex;
private boolean markAsTrusted;
@@ -371,6 +373,9 @@
* class loader. One common case for this requirement is a mock class wanting to mock package
* private methods of the original class.
*
+ * <p>If the classLoader is not a subclass of {@code dalvik.system.BaseDexClassLoader} this
+ * option is ignored.
+ *
* @param classLoader the class loader the new class should be loaded by
*/
public void setSharedClassLoader(ClassLoader classLoader) {
@@ -383,41 +388,71 @@
private ClassLoader generateClassLoader(File result, File dexCache, ClassLoader parent) {
try {
+ boolean shareClassLoader = sharedClassLoader != null;
+
+ ClassLoader preferredClassLoader = null;
+ if (parent != null) {
+ preferredClassLoader = parent;
+ } else if (sharedClassLoader != null) {
+ preferredClassLoader = sharedClassLoader;
+ }
+
+ Class baseDexClassLoaderClass = Class.forName("dalvik.system.BaseDexClassLoader");
+
+ if (shareClassLoader) {
+ if (!baseDexClassLoaderClass.isAssignableFrom(preferredClassLoader.getClass())) {
+ if (!preferredClassLoader.getClass().getName().equals(
+ "java.lang.BootClassLoader")) {
+ if (!didWarnNonBaseDexClassLoader) {
+ System.err.println("Cannot share classloader as shared classloader '"
+ + preferredClassLoader + "' is not a subclass of '"
+ + baseDexClassLoaderClass
+ + "'");
+ didWarnNonBaseDexClassLoader = true;
+ }
+ }
+
+ shareClassLoader = false;
+ }
+ }
+
// Try to load the class so that it can call hidden APIs. This is required for spying
// on system classes as real-methods of these classes might call blacklisted APIs
if (markAsTrusted) {
try {
- if (sharedClassLoader != null) {
- ClassLoader loader = parent != null ? parent : sharedClassLoader;
- loader.getClass().getMethod("addDexPath", String.class,
- Boolean.TYPE).invoke(loader, result.getPath(), true);
- return loader;
+ if (shareClassLoader) {
+ preferredClassLoader.getClass().getMethod("addDexPath", String.class,
+ Boolean.TYPE).invoke(preferredClassLoader, result.getPath(), true);
+ return preferredClassLoader;
} else {
- return (ClassLoader) Class.forName("dalvik.system.BaseDexClassLoader")
+ return (ClassLoader) baseDexClassLoaderClass
.getConstructor(String.class, File.class, String.class,
ClassLoader.class, Boolean.TYPE)
.newInstance(result.getPath(), dexCache.getAbsoluteFile(), null,
- parent, true);
+ preferredClassLoader, true);
}
} catch (InvocationTargetException e) {
if (e.getCause() instanceof SecurityException) {
- Log.i(LOG_TAG, "Cannot allow to call blacklisted super methods. This might "
- + "break spying on system classes.", e.getCause());
+ if (!didWarnBlacklistedMethods) {
+ System.err.println("Cannot allow to call blacklisted super methods. "
+ + "This might break spying on system classes." + e.getCause());
+ didWarnBlacklistedMethods = true;
+ }
} else {
throw e;
}
}
}
- if (sharedClassLoader != null) {
- ClassLoader loader = parent != null ? parent : sharedClassLoader;
- loader.getClass().getMethod("addDexPath", String.class).invoke(loader,
- result.getPath());
- return loader;
+ if (shareClassLoader) {
+ preferredClassLoader.getClass().getMethod("addDexPath", String.class).invoke(
+ preferredClassLoader, result.getPath());
+ return preferredClassLoader;
} else {
return (ClassLoader) Class.forName("dalvik.system.DexClassLoader")
.getConstructor(String.class, String.class, String.class, ClassLoader.class)
- .newInstance(result.getPath(), dexCache.getAbsolutePath(), null, parent);
+ .newInstance(result.getPath(), dexCache.getAbsolutePath(), null,
+ preferredClassLoader);
}
} catch (ClassNotFoundException e) {
throw new UnsupportedOperationException("load() requires a Dalvik VM", e);
@@ -452,7 +487,8 @@
* property.
*
* @param parent the parent ClassLoader to be used when loading our
- * generated types
+ * generated types (if set, overrides
+ * {@link #setSharedClassLoader(ClassLoader) shared class loader}.
* @param dexCache the destination directory where generated and optimized
* dex files will be written. If null, this class will try to guess the
* application's private data dir.
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 053fb16..5e4300c 100644
--- a/dexmaker/src/main/java/com/android/dx/stock/ProxyBuilder.java
+++ b/dexmaker/src/main/java/com/android/dx/stock/ProxyBuilder.java
@@ -43,12 +43,11 @@
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
+import static java.lang.reflect.Modifier.ABSTRACT;
import static java.lang.reflect.Modifier.PRIVATE;
import static java.lang.reflect.Modifier.PUBLIC;
import static java.lang.reflect.Modifier.STATIC;
-import android.os.Build;
-
/**
* Creates dynamic proxies of concrete classes.
* <p>
@@ -132,8 +131,8 @@
* Android's runtime doesn't support class unloading so there's little
* value in using weak references.
*/
- private static final Map<Class<?>, Class<?>> generatedProxyClasses
- = Collections.synchronizedMap(new HashMap<Class<?>, Class<?>>());
+ private static final Map<ProxiedClass<?>, Class<?>> generatedProxyClasses
+ = Collections.synchronizedMap(new HashMap<ProxiedClass<?>, Class<?>>());
private final Class<T> baseClass;
private ClassLoader parentClassLoader = ProxyBuilder.class.getClassLoader();
@@ -144,6 +143,7 @@
private Set<Class<?>> interfaces = new HashSet<>();
private Method[] methods;
private boolean sharedClassLoader;
+ private boolean markTrusted;
private ProxyBuilder(Class<T> clazz) {
baseClass = clazz;
@@ -209,6 +209,11 @@
return this;
}
+ public ProxyBuilder<T> markTrusted() {
+ this.markTrusted = true;
+ return this;
+ }
+
/**
* Create a new instance of the class to proxy.
*
@@ -258,22 +263,20 @@
* {@link #setInvocationHandler(Object, InvocationHandler)}.
*/
public Class<? extends T> buildProxyClass() throws IOException {
+ ClassLoader requestedClassloader;
+ if (sharedClassLoader) {
+ requestedClassloader = baseClass.getClassLoader();
+ } else {
+ requestedClassloader = parentClassLoader;
+ }
+
// try the cache to see if we've generated this one before
// we only populate the map with matching types
@SuppressWarnings("unchecked")
- Class<? extends T> proxyClass = (Class) generatedProxyClasses.get(baseClass);
- if (proxyClass != null) {
- boolean validClassLoader;
- if (sharedClassLoader) {
- ClassLoader parent = parentClassLoader != null ? parentClassLoader : baseClass
- .getClassLoader();
- validClassLoader = proxyClass.getClassLoader() == parent;
- } else {
- validClassLoader = proxyClass.getClassLoader().getParent() == parentClassLoader;
- }
- if (validClassLoader && interfaces.equals(asSet(proxyClass.getInterfaces()))) {
- return proxyClass; // cache hit!
- }
+ Class<? extends T> proxyClass = (Class) generatedProxyClasses.get(
+ new ProxiedClass<>(baseClass, requestedClassloader, sharedClassLoader));
+ if (proxyClass != null && interfaces.equals(asSet(proxyClass.getInterfaces()))) {
+ return proxyClass; // cache hit!
}
// the cache missed; generate the class
@@ -301,9 +304,9 @@
generateCodeForAllMethods(dexMaker, generatedType, methodsToProxy, superType);
dexMaker.declare(generatedType, generatedName + ".generated", PUBLIC, superType, getInterfacesAsTypeIds());
if (sharedClassLoader) {
- dexMaker.setSharedClassLoader(baseClass.getClassLoader());
+ dexMaker.setSharedClassLoader(requestedClassloader);
}
- if (Build.VERSION.SDK_INT >= 28) {
+ if (markTrusted) {
// The proxied class might have blacklisted methods. Blacklisting methods (and fields)
// is a new feature of Android P:
//
@@ -315,7 +318,12 @@
// all generated classes as trusted.
dexMaker.markAsTrusted();
}
- ClassLoader classLoader = dexMaker.generateAndLoad(parentClassLoader, dexCache);
+ ClassLoader classLoader;
+ if (sharedClassLoader) {
+ classLoader = dexMaker.generateAndLoad(null, dexCache);
+ } else {
+ classLoader = dexMaker.generateAndLoad(parentClassLoader, dexCache);
+ }
try {
proxyClass = loadClass(classLoader, generatedName);
} catch (IllegalAccessError e) {
@@ -327,7 +335,9 @@
throw new AssertionError(e);
}
setMethodsStaticField(proxyClass, methodsToProxy);
- generatedProxyClasses.put(baseClass, proxyClass);
+ generatedProxyClasses.put(new ProxiedClass<>(baseClass, requestedClassloader,
+ sharedClassLoader),
+ proxyClass);
return proxyClass;
}
@@ -424,6 +434,36 @@
}
}
+ /**
+ * Add
+ *
+ * <pre>
+ * abstractMethodErrorMessage = method + " cannot be called";
+ * abstractMethodError = new AbstractMethodError(abstractMethodErrorMessage);
+ * throw abstractMethodError;
+ * </pre>
+ *
+ * to the {@code code}.
+ *
+ * @param code The code to add to
+ * @param method The method that is abstract
+ * @param abstractMethodErrorMessage The {@link Local} to store the error message
+ * @param abstractMethodError The {@link Local} to store the error object
+ */
+ private static void throwAbstractMethodError(Code code, Method method,
+ Local<String> abstractMethodErrorMessage,
+ Local<AbstractMethodError> abstractMethodError) {
+ TypeId<AbstractMethodError> abstractMethodErrorClass = TypeId.get(AbstractMethodError.class);
+
+ MethodId<AbstractMethodError, Void> abstractMethodErrorConstructor =
+ abstractMethodErrorClass.getConstructor(TypeId.STRING);
+ code.loadConstant(abstractMethodErrorMessage, "'" + method + "' cannot be called");
+ code.newInstance(abstractMethodError, abstractMethodErrorConstructor,
+ abstractMethodErrorMessage);
+
+ code.throwValue(abstractMethodError);
+ }
+
private static <T, G extends T> void generateCodeForAllMethods(DexMaker dexMaker,
TypeId<G> generatedType, Method[] methodsToProxy, TypeId<T> superclassType) {
TypeId<InvocationHandler> handlerType = TypeId.get(InvocationHandler.class);
@@ -445,33 +485,10 @@
* ...
* }
*
- * Then the following code will generate a method on the proxy that looks something
- * like this:
+ * Then the following dex byte code will generate a method on the proxy that looks
+ * something like this (in idiomatic Java):
*
- * public int doSomething(Bar param0, int param1) {
- * int methodIndex = 4;
- * Method[] allMethods = Example_Proxy.$__methodArray;
- * Method thisMethod = allMethods[methodIndex];
- * int argsLength = 2;
- * Object[] args = new Object[argsLength];
- * InvocationHandler localHandler = this.$__handler;
- * // for-loop begins
- * int p = 0;
- * Bar parameter0 = param0;
- * args[p] = parameter0;
- * p = 1;
- * int parameter1 = param1;
- * Integer boxed1 = Integer.valueOf(parameter1);
- * args[p] = boxed1;
- * // for-loop ends
- * Object result = localHandler.invoke(this, thisMethod, args);
- * Integer castResult = (Integer) result;
- * int unboxedResult = castResult.intValue();
- * return unboxedResult;
- * }
- *
- * Or, in more idiomatic Java:
- *
+ * // if doSomething is not abstract
* public int doSomething(Bar param0, int param1) {
* if ($__handler == null) {
* return super.doSomething(param0, param1);
@@ -479,6 +496,15 @@
* return __handler.invoke(this, __methodArray[4],
* new Object[] { param0, Integer.valueOf(param1) });
* }
+ *
+ * // if doSomething is abstract
+ * public int doSomething(Bar param0, int param1) {
+ * if ($__handler == null) {
+ * throw new AbstractMethodError("'doSomething' cannot be called");
+ * }
+ * return __handler.invoke(this, __methodArray[4],
+ * new Object[] { param0, Integer.valueOf(param1) });
+ * }
*/
Method method = methodsToProxy[m];
String name = method.getName();
@@ -489,8 +515,9 @@
}
Class<?> returnType = method.getReturnType();
TypeId<?> resultType = TypeId.get(returnType);
- MethodId<T, ?> superMethod = superclassType.getMethod(resultType, name, argTypes);
MethodId<?, ?> methodId = generatedType.getMethod(resultType, name, argTypes);
+ TypeId<AbstractMethodError> abstractMethodErrorClass =
+ TypeId.get(AbstractMethodError.class);
Code code = dexMaker.declare(methodId, PUBLIC);
Local<G> localThis = code.getThis(generatedType);
Local<InvocationHandler> localHandler = code.newLocal(handlerType);
@@ -508,10 +535,22 @@
if (aBoxedClass != null) {
aBoxedResult = code.newLocal(TypeId.get(aBoxedClass));
}
- Local<?>[] superArgs2 = new Local<?>[argClasses.length];
- Local<?> superResult2 = code.newLocal(resultType);
Local<InvocationHandler> nullHandler = code.newLocal(handlerType);
+ Local<?>[] superArgs2 = null;
+ Local<?> superResult2 = null;
+ MethodId<T, ?> superMethod = null;
+ Local<String> abstractMethodErrorMessage = null;
+ Local<AbstractMethodError> abstractMethodError = null;
+ if ((method.getModifiers() & ABSTRACT) == 0) {
+ superArgs2 = new Local<?>[argClasses.length];
+ superResult2 = code.newLocal(resultType);
+ superMethod = superclassType.getMethod(resultType, name, argTypes);
+ } else {
+ abstractMethodErrorMessage = code.newLocal(TypeId.STRING);
+ abstractMethodError = code.newLocal(abstractMethodErrorClass);
+ }
+
code.loadConstant(methodIndex, m);
code.sget(allMethods, methodArray);
code.aget(thisMethod, methodArray, methodIndex);
@@ -541,15 +580,21 @@
// This is required to handle the case of construction of an object which leaks the
// "this" pointer.
code.mark(handlerNullCase);
- for (int i = 0; i < superArgs2.length; ++i) {
- superArgs2[i] = code.getParameter(i, argTypes[i]);
- }
- if (void.class.equals(returnType)) {
- code.invokeSuper(superMethod, null, localThis, superArgs2);
- code.returnVoid();
+
+ if ((method.getModifiers() & ABSTRACT) == 0) {
+ for (int i = 0; i < superArgs2.length; ++i) {
+ superArgs2[i] = code.getParameter(i, argTypes[i]);
+ }
+ if (void.class.equals(returnType)) {
+ code.invokeSuper(superMethod, null, localThis, superArgs2);
+ code.returnVoid();
+ } else {
+ invokeSuper(superMethod, code, localThis, superArgs2, superResult2);
+ code.returnValue(superResult2);
+ }
} else {
- invokeSuper(superMethod, code, localThis, superArgs2, superResult2);
- code.returnValue(superResult2);
+ throwAbstractMethodError(code, method, abstractMethodErrorMessage,
+ abstractMethodError);
}
/*
@@ -560,22 +605,29 @@
* return result;
* }
*/
- // TODO: don't include a super_ method if the target is abstract!
MethodId<G, ?> callsSuperMethod = generatedType.getMethod(
resultType, superMethodName(method), argTypes);
Code superCode = dexMaker.declare(callsSuperMethod, PUBLIC);
- Local<G> superThis = superCode.getThis(generatedType);
- Local<?>[] superArgs = new Local<?>[argClasses.length];
- for (int i = 0; i < superArgs.length; ++i) {
- superArgs[i] = superCode.getParameter(i, argTypes[i]);
- }
- if (void.class.equals(returnType)) {
- superCode.invokeSuper(superMethod, null, superThis, superArgs);
- superCode.returnVoid();
+ if ((method.getModifiers() & ABSTRACT) == 0) {
+ Local<G> superThis = superCode.getThis(generatedType);
+ Local<?>[] superArgs = new Local<?>[argClasses.length];
+ for (int i = 0; i < superArgs.length; ++i) {
+ superArgs[i] = superCode.getParameter(i, argTypes[i]);
+ }
+ if (void.class.equals(returnType)) {
+ superCode.invokeSuper(superMethod, null, superThis, superArgs);
+ superCode.returnVoid();
+ } else {
+ Local<?> superResult = superCode.newLocal(resultType);
+ invokeSuper(superMethod, superCode, superThis, superArgs, superResult);
+ superCode.returnValue(superResult);
+ }
} else {
- Local<?> superResult = superCode.newLocal(resultType);
- invokeSuper(superMethod, superCode, superThis, superArgs, superResult);
- superCode.returnValue(superResult);
+ Local<String> superAbstractMethodErrorMessage = superCode.newLocal(TypeId.STRING);
+ Local<AbstractMethodError> superAbstractMethodError = superCode.newLocal
+ (abstractMethodErrorClass);
+ throwAbstractMethodError(superCode, method, superAbstractMethodErrorMessage,
+ superAbstractMethodError);
}
}
}
@@ -883,4 +935,48 @@
return result;
}
}
+
+ /**
+ * A class that was already proxied.
+ */
+ private static class ProxiedClass<U> {
+ final Class<U> clazz;
+
+ /**
+ * Class loader requested when the proxy class was generated. This might not be the
+ * class loader of {@code clazz} as not all class loaders can be shared.
+ *
+ * @see DexMaker#generateClassLoader(File, File, ClassLoader)
+ */
+ final ClassLoader requestedClassloader;
+
+ final boolean sharedClassLoader;
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (other == null || getClass() != other.getClass()) {
+ return false;
+ }
+
+ ProxiedClass<?> that = (ProxiedClass<?>) other;
+ return clazz == that.clazz
+ && requestedClassloader == that.requestedClassloader
+ && sharedClassLoader == that.sharedClassLoader;
+ }
+
+ @Override
+ public int hashCode() {
+ return clazz.hashCode() + requestedClassloader.hashCode() + (sharedClassLoader ? 1 : 0);
+ }
+
+ private ProxiedClass(Class<U> clazz, ClassLoader requestedClassloader,
+ boolean sharedClassLoader) {
+ this.clazz = clazz;
+ this.requestedClassloader = requestedClassloader;
+ this.sharedClassLoader = sharedClassLoader;
+ }
+ }
}
diff --git a/update_source.sh b/update_source.sh
index ff7cc66..fc0c053 100755
--- a/update_source.sh
+++ b/update_source.sh
@@ -23,13 +23,18 @@
SOURCE="https://github.com/linkedin/dexmaker"
INCLUDE="
LICENSE
+
dexmaker
dexmaker-mockito
- dexmaker-mockito-tests
dexmaker-mockito-inline
+ dexmaker-mockito-inline-extended
+
dexmaker-mockito-inline-dispatcher
- dexmaker-mockito-inline-tests/src
- dexmaker-tests/src
+
+ dexmaker-tests
+ dexmaker-mockito-tests
+ dexmaker-mockito-inline-tests
+ dexmaker-mockito-inline-extended-tests
"
EXCLUDE="
@@ -58,11 +63,6 @@
rm -r $exclude
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