From 3ee0f9ded8dab4f18eddb0e1b98343b57d374088 Mon Sep 17 00:00:00 2001 From: Joshua Selbo Date: Thu, 14 May 2026 15:20:40 -0500 Subject: [PATCH 1/5] Fix dexmaker-mockito-inline integration tests --- .../src/main/java/com/android/dx/mockito/inline/JvmtiAgent.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 45194df..2c8898f 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 @@ -89,7 +89,7 @@ private static String resolveAgentPath(ClassLoader cl) throws IOException { copiedAgent.deleteOnExit(); InputStream is; - if (path != null) { + if (path != null && new File(path).isFile()) { is = new FileInputStream(path); } else { // findLibrary returned null — try loading from APK resources From adf586cfb3a5dc322aaa15b05cdfc9eb3eea34bd Mon Sep 17 00:00:00 2001 From: Joshua Selbo Date: Thu, 14 May 2026 15:23:10 -0500 Subject: [PATCH 2/5] Upgrade AGP to 8.13.2 --- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 7e90ffb..f6f3ee9 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.10.0' + classpath 'com.android.tools.build:gradle:8.13.2' } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 81aa1c0..2733ed5 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From d9278b2aef67d7140e3adb1854fff01249652ffb Mon Sep 17 00:00:00 2001 From: Joshua Selbo Date: Thu, 14 May 2026 15:35:47 -0500 Subject: [PATCH 3/5] Upgrade Mockito to 5.X --- .../build.gradle | 2 +- .../inline/extended/tests/MockStatic.java | 4 +-- ...ticMockitoSessionVsMockitoJUnitRunner.java | 2 +- .../inline/extended/tests/VerifyStatic.java | 4 +-- dexmaker-mockito-inline-extended/build.gradle | 2 +- .../inline/extended/StaticInOrder.java | 7 +++++ dexmaker-mockito-inline-tests/build.gradle | 2 +- dexmaker-mockito-inline/build.gradle | 2 +- .../android/dx/mockito/inline/JvmtiAgent.java | 2 +- .../mockito/inline/MockMakerMultiplexer.java | 27 +++++++++++-------- dexmaker-mockito-tests/build.gradle | 2 +- dexmaker-mockito/build.gradle | 2 +- gradle/libs.versions.toml | 5 ++++ 13 files changed, 40 insertions(+), 23 deletions(-) create mode 100644 gradle/libs.versions.toml diff --git a/dexmaker-mockito-inline-extended-tests/build.gradle b/dexmaker-mockito-inline-extended-tests/build.gradle index f77c080..d76a84f 100644 --- a/dexmaker-mockito-inline-extended-tests/build.gradle +++ b/dexmaker-mockito-inline-extended-tests/build.gradle @@ -30,5 +30,5 @@ dependencies { androidTestImplementation 'androidx.test:runner:1.4.0' androidTestImplementation 'androidx.test:rules:1.4.0' - androidTestImplementation 'org.mockito:mockito-core:2.28.2', { exclude group: 'net.bytebuddy' } + androidTestImplementation libs.mockito.core, { exclude group: 'net.bytebuddy' } } diff --git a/dexmaker-mockito-inline-extended-tests/src/androidTest/java/com/android/dx/mockito/inline/extended/tests/MockStatic.java b/dexmaker-mockito-inline-extended-tests/src/androidTest/java/com/android/dx/mockito/inline/extended/tests/MockStatic.java index 95a9baa..9fe2ea0 100644 --- a/dexmaker-mockito-inline-extended-tests/src/androidTest/java/com/android/dx/mockito/inline/extended/tests/MockStatic.java +++ b/dexmaker-mockito-inline-extended-tests/src/androidTest/java/com/android/dx/mockito/inline/extended/tests/MockStatic.java @@ -25,7 +25,7 @@ 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.verifyNoInteractions; import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; import static org.junit.Assert.assertEquals; @@ -401,7 +401,7 @@ public void clearInvocationsRemovedInvocations() throws Exception { try { SuperClass.returnB(); clearInvocations(staticMockMarker(SuperClass.class)); - verifyZeroInteractions(staticMockMarker(SuperClass.class)); + verifyNoInteractions(staticMockMarker(SuperClass.class)); } finally { session.finishMocking(); } diff --git a/dexmaker-mockito-inline-extended-tests/src/androidTest/java/com/android/dx/mockito/inline/extended/tests/StaticMockitoSessionVsMockitoJUnitRunner.java b/dexmaker-mockito-inline-extended-tests/src/androidTest/java/com/android/dx/mockito/inline/extended/tests/StaticMockitoSessionVsMockitoJUnitRunner.java index fe75755..e2209b3 100644 --- a/dexmaker-mockito-inline-extended-tests/src/androidTest/java/com/android/dx/mockito/inline/extended/tests/StaticMockitoSessionVsMockitoJUnitRunner.java +++ b/dexmaker-mockito-inline-extended-tests/src/androidTest/java/com/android/dx/mockito/inline/extended/tests/StaticMockitoSessionVsMockitoJUnitRunner.java @@ -20,7 +20,7 @@ import org.junit.runner.RunWith; import org.mockito.junit.MockitoJUnitRunner; -@RunWith(MockitoJUnitRunner.class) +@RunWith(MockitoJUnitRunner.Silent.class) public class StaticMockitoSessionVsMockitoJUnitRunner { @Test public void simpleStubbing() throws Exception { diff --git a/dexmaker-mockito-inline-extended-tests/src/androidTest/java/com/android/dx/mockito/inline/extended/tests/VerifyStatic.java b/dexmaker-mockito-inline-extended-tests/src/androidTest/java/com/android/dx/mockito/inline/extended/tests/VerifyStatic.java index afb1439..6496ec4 100644 --- a/dexmaker-mockito-inline-extended-tests/src/androidTest/java/com/android/dx/mockito/inline/extended/tests/VerifyStatic.java +++ b/dexmaker-mockito-inline-extended-tests/src/androidTest/java/com/android/dx/mockito/inline/extended/tests/VerifyStatic.java @@ -30,7 +30,7 @@ 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.verifyNoInteractions; import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; @@ -180,7 +180,7 @@ public void zeroInvocationsThrowsIfThereWasAnInvocation() throws Exception { MockitoSession session = mockitoSession().mockStatic(EchoClass.class).startMocking(); try { EchoClass.echo("marco!"); - verifyZeroInteractions(staticMockMarker(EchoClass.class)); + verifyNoInteractions(staticMockMarker(EchoClass.class)); fail(); } finally { session.finishMocking(); diff --git a/dexmaker-mockito-inline-extended/build.gradle b/dexmaker-mockito-inline-extended/build.gradle index e0182ed..bf59f95 100644 --- a/dexmaker-mockito-inline-extended/build.gradle +++ b/dexmaker-mockito-inline-extended/build.gradle @@ -46,5 +46,5 @@ dependencies { implementation project(':dexmaker-mockito-inline') - api 'org.mockito:mockito-core:2.28.2', { exclude group: 'net.bytebuddy' } + api libs.mockito.core, { exclude group: 'net.bytebuddy' } } 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 index 312a454..7950379 100644 --- 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 @@ -17,6 +17,7 @@ package com.android.dx.mockito.inline.extended; import org.mockito.InOrder; +import org.mockito.MockedStatic; import org.mockito.Mockito; import org.mockito.verification.VerificationMode; @@ -141,6 +142,12 @@ public void verify(MockedMethod method, VerificationMode mode) { verify((MockedVoidMethod) method::get, mode); } + @Override + public void verify(MockedStatic mockedStatic, MockedStatic.Verification verification, + VerificationMode mode) { + instanceInOrder.verify(mockedStatic, verification, mode); + } + @Override public void verifyNoMoreInteractions() { instanceInOrder.verifyNoMoreInteractions(); diff --git a/dexmaker-mockito-inline-tests/build.gradle b/dexmaker-mockito-inline-tests/build.gradle index 7beaf71..30b3208 100644 --- a/dexmaker-mockito-inline-tests/build.gradle +++ b/dexmaker-mockito-inline-tests/build.gradle @@ -30,5 +30,5 @@ dependencies { androidTestImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test:runner:1.4.0' - androidTestImplementation 'org.mockito:mockito-core:2.28.2', { exclude group: 'net.bytebuddy' } + androidTestImplementation libs.mockito.core, { exclude group: 'net.bytebuddy' } } diff --git a/dexmaker-mockito-inline/build.gradle b/dexmaker-mockito-inline/build.gradle index d077f5d..deb4e7f 100644 --- a/dexmaker-mockito-inline/build.gradle +++ b/dexmaker-mockito-inline/build.gradle @@ -41,5 +41,5 @@ dependencies { implementation project(':dexmaker') - api 'org.mockito:mockito-core:2.28.2', { exclude group: 'net.bytebuddy' } + api libs.mockito.core, { exclude group: 'net.bytebuddy' } } 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 2c8898f..3eb4a30 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 @@ -80,7 +80,7 @@ class JvmtiAgent { private static String resolveAgentPath(ClassLoader cl) throws IOException { String path = ((BaseDexClassLoader) cl).findLibrary("dexmakerjvmtiagent"); - if (path != null && !path.contains("=")) { + if (path != null && !path.contains("=") && !path.contains("!")) { return path; } 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 index 8949d46..f2ed25c 100644 --- 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 @@ -23,7 +23,6 @@ import org.mockito.plugins.InlineMockMaker; import org.mockito.plugins.MockMaker; -import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; /** @@ -36,7 +35,7 @@ public final class MockMakerMultiplexer implements InlineMockMaker { static { String[] potentialMockMakers = new String[] { "com.android.dx.mockito.inline.InlineStaticMockMaker", - InlineDexmakerMockMaker.class.getName() + "com.android.dx.mockito.inline.InlineDexmakerMockMaker" }; ArrayList mockMakers = new ArrayList<>(); @@ -45,14 +44,10 @@ public final class MockMakerMultiplexer implements InlineMockMaker { Class mockMakerClass = (Class) 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); - } + } catch (Exception e) { + Log.e(LOG_TAG, "Could not init mockmaker " + potentialMockMaker, e); + } catch (Error e) { + Log.e(LOG_TAG, "Could not init mockmaker " + potentialMockMaker, e); } } @@ -102,7 +97,17 @@ public TypeMockability isTypeMockable(Class type) { } } - return null; + return new TypeMockability() { + @Override + public boolean mockable() { + return false; + } + + @Override + public String nonMockableReason() { + return "No mock makers available to mock this type"; + } + }; } @Override diff --git a/dexmaker-mockito-tests/build.gradle b/dexmaker-mockito-tests/build.gradle index 90d98f8..5477fd2 100644 --- a/dexmaker-mockito-tests/build.gradle +++ b/dexmaker-mockito-tests/build.gradle @@ -24,5 +24,5 @@ dependencies { androidTestImplementation 'androidx.test:runner:1.4.0' androidTestImplementation 'junit:junit:4.13.2' - androidTestImplementation 'org.mockito:mockito-core:2.28.2', { exclude group: 'net.bytebuddy' } + androidTestImplementation libs.mockito.core, { exclude group: 'net.bytebuddy' } } diff --git a/dexmaker-mockito/build.gradle b/dexmaker-mockito/build.gradle index 1354cc6..5b284f8 100644 --- a/dexmaker-mockito/build.gradle +++ b/dexmaker-mockito/build.gradle @@ -22,5 +22,5 @@ dependencies { implementation project(':dexmaker') - api 'org.mockito:mockito-core:2.28.2', { exclude group: 'net.bytebuddy' } + api libs.mockito.core, { exclude group: 'net.bytebuddy' } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..d7fd4af --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,5 @@ +[versions] +mockito = "5.23.0" + +[libraries] +mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito" } From 75cfa350a9f12b132698e5a58ad3bea2c3f3096a Mon Sep 17 00:00:00 2001 From: Joshua Selbo Date: Thu, 14 May 2026 16:06:44 -0500 Subject: [PATCH 4/5] Implement createSingletonMock in InlineDexmakerMockMaker Add singleton mock support to dexmaker, enabling Mockito's mockSingleton() API on Android. Singleton mocks are thread-local and intercept method calls on existing instances without creating new proxy objects. --- .../mockito/inline/tests/MockSingleton.java | 141 ++++++++++++++++++ .../dx/mockito/inline/ClassTransformer.java | 5 +- .../inline/InlineDexmakerMockMaker.java | 64 +++++++- .../android/dx/mockito/inline/JvmtiAgent.java | 2 +- .../mockito/inline/MockMakerMultiplexer.java | 30 +++- .../dx/mockito/inline/MockMethodAdvice.java | 20 ++- 6 files changed, 249 insertions(+), 13 deletions(-) create mode 100644 dexmaker-mockito-inline-tests/src/androidTest/java/com/android/dx/mockito/inline/tests/MockSingleton.java diff --git a/dexmaker-mockito-inline-tests/src/androidTest/java/com/android/dx/mockito/inline/tests/MockSingleton.java b/dexmaker-mockito-inline-tests/src/androidTest/java/com/android/dx/mockito/inline/tests/MockSingleton.java new file mode 100644 index 0000000..55a2fa1 --- /dev/null +++ b/dexmaker-mockito-inline-tests/src/androidTest/java/com/android/dx/mockito/inline/tests/MockSingleton.java @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dx.mockito.inline.tests; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.mockito.Mockito.mockSingleton; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockedSingleton; + +@RunWith(AndroidJUnit4.class) +public class MockSingleton { + + enum SingleEnum { + VALUE_A { + @Override + String greeting() { + return "hello"; + } + }, + VALUE_B { + @Override + String greeting() { + return "world"; + } + }; + + abstract String greeting(); + + int compute(int x) { + return x + 1; + } + } + + static class SimpleSingleton { + static final SimpleSingleton INSTANCE = new SimpleSingleton(); + + String getValue() { + return "original"; + } + + int add(int a, int b) { + return a + b; + } + } + + @Test + public void mockEnumSingleton() { + assertEquals("hello", SingleEnum.VALUE_A.greeting()); + + try (MockedSingleton mocked = mockSingleton(SingleEnum.VALUE_A)) { + when(SingleEnum.VALUE_A.greeting()).thenReturn("mocked"); + assertEquals("mocked", SingleEnum.VALUE_A.greeting()); + } + + assertEquals("hello", SingleEnum.VALUE_A.greeting()); + } + + @Test + public void mockEnumDoesNotAffectOtherValues() { + try (MockedSingleton mocked = mockSingleton(SingleEnum.VALUE_A)) { + when(SingleEnum.VALUE_A.greeting()).thenReturn("mocked"); + + assertEquals("mocked", SingleEnum.VALUE_A.greeting()); + assertEquals("world", SingleEnum.VALUE_B.greeting()); + } + } + + @Test + public void mockSingletonInstance() { + assertEquals("original", SimpleSingleton.INSTANCE.getValue()); + + try (MockedSingleton mocked = mockSingleton(SimpleSingleton.INSTANCE)) { + when(SimpleSingleton.INSTANCE.getValue()).thenReturn("mocked"); + assertEquals("mocked", SimpleSingleton.INSTANCE.getValue()); + } + + assertEquals("original", SimpleSingleton.INSTANCE.getValue()); + } + + @Test + public void getInstanceReturnsSameObject() { + try (MockedSingleton mocked = mockSingleton(SingleEnum.VALUE_A)) { + assertSame(SingleEnum.VALUE_A, mocked.getInstance()); + } + } + + @Test + public void resetClearsStubs() { + try (MockedSingleton mocked = mockSingleton(SingleEnum.VALUE_A)) { + when(SingleEnum.VALUE_A.greeting()).thenReturn("mocked"); + assertEquals("mocked", SingleEnum.VALUE_A.greeting()); + + reset(SingleEnum.VALUE_A); + // After reset, default mock behavior returns null for objects + assertEquals(null, SingleEnum.VALUE_A.greeting()); + } + } + + @Test + public void verifyInteraction() { + try (MockedSingleton mocked = mockSingleton(SimpleSingleton.INSTANCE)) { + SimpleSingleton.INSTANCE.getValue(); + + verify(SimpleSingleton.INSTANCE).getValue(); + } + } + + @Test + public void stubMethodWithArgs() { + try (MockedSingleton mocked = mockSingleton(SimpleSingleton.INSTANCE)) { + when(SimpleSingleton.INSTANCE.add(2, 3)).thenReturn(99); + + assertEquals(99, SimpleSingleton.INSTANCE.add(2, 3)); + assertEquals(0, SimpleSingleton.INSTANCE.add(1, 1)); + } + + assertEquals(2, SimpleSingleton.INSTANCE.add(1, 1)); + } +} 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 89f713a..fa43355 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 @@ -94,11 +94,12 @@ class ClassTransformer { * mocked or not. */ ClassTransformer(JvmtiAgent agent, Class dispatcherClass, - Map mocks) { + Map mocks, + ThreadLocal> singletonMocks) { this.agent = agent; mockedTypes = Collections.synchronizedSet(new HashSet>()); identifier = String.valueOf(System.identityHashCode(this)); - MockMethodAdvice advice = new MockMethodAdvice(mocks); + MockMethodAdvice advice = new MockMethodAdvice(mocks, singletonMocks); try { dispatcherClass.getMethod("set", String.class, Object.class).invoke(null, identifier, 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 0a594e0..4f7a8d6 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 @@ -44,8 +44,10 @@ import java.lang.reflect.Proxy; import java.util.AbstractMap; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.IdentityHashMap; import java.util.Map; import java.util.Set; @@ -167,6 +169,9 @@ public final class InlineDexmakerMockMaker implements InlineMockMaker { */ private final Map mocks; + private final ThreadLocal> singletonMocks = + new ThreadLocal<>(); + /** * Class doing the actual byte code transformation. */ @@ -185,7 +190,7 @@ public InlineDexmakerMockMaker() { } mocks = new MockMap(); - classTransformer = new ClassTransformer(AGENT, DISPATCHER_CLASS, mocks); + classTransformer = new ClassTransformer(AGENT, DISPATCHER_CLASS, mocks, singletonMocks); } /** @@ -362,6 +367,54 @@ public MockHandler getHandler(Object mock) { return adapter != null ? adapter.getHandler() : null; } + @Override + public SingletonMockControl createSingletonMock(T instance, MockCreationSettings settings, MockHandler handler) { + Class typeToMock = settings.getTypeToMock(); + Set> interfacesSet = settings.getExtraInterfaces(); + + classTransformer.mockClass(MockFeatures.withMockFeatures(typeToMock, interfacesSet)); + + InvocationHandlerAdapter handlerAdapter = new InvocationHandlerAdapter(handler); + + return new InlineSingletonMockControl<>(instance, handlerAdapter); + } + + private class InlineSingletonMockControl implements SingletonMockControl { + private final T instance; + private final InvocationHandlerAdapter handlerAdapter; + + InlineSingletonMockControl(T instance, InvocationHandlerAdapter handlerAdapter) { + this.instance = instance; + this.handlerAdapter = handlerAdapter; + } + + @Override + public T getInstance() { + return instance; + } + + @Override + public void enable() { + Map singletons = singletonMocks.get(); + if (singletons == null) { + singletons = new IdentityHashMap<>(); + singletonMocks.set(singletons); + } + singletons.put(instance, handlerAdapter); + } + + @Override + public void disable() { + Map singletons = singletonMocks.get(); + if (singletons != null) { + singletons.remove(instance); + if (singletons.isEmpty()) { + singletonMocks.remove(); + } + } + } + } + /** * Get the {@link InvocationHandlerAdapter} registered for a mock. * @@ -374,7 +427,14 @@ private InvocationHandlerAdapter getInvocationHandlerAdapter(Object instance) { return null; } - return mocks.get(instance); + InvocationHandlerAdapter adapter = mocks.get(instance); + if (adapter == null) { + Map singletons = singletonMocks.get(); + if (singletons != null) { + adapter = singletons.get(instance); + } + } + return adapter; } /** 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 3eb4a30..2c8898f 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 @@ -80,7 +80,7 @@ class JvmtiAgent { private static String resolveAgentPath(ClassLoader cl) throws IOException { String path = ((BaseDexClassLoader) cl).findLibrary("dexmakerjvmtiagent"); - if (path != null && !path.contains("=") && !path.contains("!")) { + if (path != null && !path.contains("=")) { return path; } 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 index f2ed25c..3147698 100644 --- 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 @@ -18,11 +18,13 @@ import android.util.Log; +import org.mockito.exceptions.base.MockitoException; import org.mockito.invocation.MockHandler; import org.mockito.mock.MockCreationSettings; import org.mockito.plugins.InlineMockMaker; import org.mockito.plugins.MockMaker; +import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; /** @@ -35,7 +37,7 @@ public final class MockMakerMultiplexer implements InlineMockMaker { static { String[] potentialMockMakers = new String[] { "com.android.dx.mockito.inline.InlineStaticMockMaker", - "com.android.dx.mockito.inline.InlineDexmakerMockMaker" + InlineDexmakerMockMaker.class.getName() }; ArrayList mockMakers = new ArrayList<>(); @@ -44,10 +46,14 @@ public final class MockMakerMultiplexer implements InlineMockMaker { Class mockMakerClass = (Class) Class.forName(potentialMockMaker); mockMakers.add(mockMakerClass.getDeclaredConstructor().newInstance()); - } catch (Exception e) { - Log.e(LOG_TAG, "Could not init mockmaker " + potentialMockMaker, e); - } catch (Error e) { - Log.e(LOG_TAG, "Could not init mockmaker " + potentialMockMaker, e); + } 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); + } } } @@ -133,4 +139,18 @@ public void clearAllMocks() { inlineMockMaker.clearAllMocks(); } } + + @Override + public SingletonMockControl createSingletonMock( + T instance, MockCreationSettings settings, MockHandler handler) { + for (MockMaker mockMaker : MOCK_MAKERS) { + try { + return mockMaker.createSingletonMock(instance, settings, handler); + } catch (MockitoException ignored) { + } + } + throw new MockitoException( + "The used MockMaker MockMakerMultiplexer does not support the creation of " + + "singleton mocks\n\nEnsure your MockMaker implementation supports this feature."); + } } 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 72fa59d..31b4279 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 @@ -22,6 +22,7 @@ */ class MockMethodAdvice { private final Map interceptors; + private final ThreadLocal> singletonInterceptors; /** Pattern to decompose a instrumentedMethodWithTypeAndSignature */ private final Pattern methodPattern = Pattern.compile("(.*)#(.*)\\((.*)\\)"); @@ -32,8 +33,21 @@ class MockMethodAdvice { @SuppressWarnings("ThreadLocalUsage") private final SelfCallInfo selfCallInfo = new SelfCallInfo(); - MockMethodAdvice(Map interceptors) { + MockMethodAdvice(Map interceptors, + ThreadLocal> singletonInterceptors) { this.interceptors = interceptors; + this.singletonInterceptors = singletonInterceptors; + } + + private InvocationHandlerAdapter getInterceptor(Object instance) { + InvocationHandlerAdapter interceptor = interceptors.get(instance); + if (interceptor == null && singletonInterceptors != null) { + Map singletons = singletonInterceptors.get(); + if (singletons != null) { + interceptor = singletons.get(instance); + } + } + return interceptor; } /** @@ -225,7 +239,7 @@ public Method getOrigin(Object instance, String methodWithTypeAndSignature) thro */ @SuppressWarnings("unused") public Callable handle(Object instance, Method origin, Object[] arguments) throws Throwable { - InvocationHandlerAdapter interceptor = interceptors.get(instance); + InvocationHandlerAdapter interceptor = getInterceptor(instance); if (interceptor == null) { return null; } @@ -242,7 +256,7 @@ public Callable handle(Object instance, Method origin, Object[] arguments) th * @return {@code true} iff the instance is a mock */ public boolean isMock(Object instance) { - return interceptors.containsKey(instance); + return getInterceptor(instance) != null; } /** From 3432470bb3ec18b2302571ccd3be22a63312c70c Mon Sep 17 00:00:00 2001 From: Joshua Selbo Date: Tue, 19 May 2026 11:28:00 -0500 Subject: [PATCH 5/5] Fix activity lint --- .../src/androidTest/AndroidManifest.xml | 8 ++++++++ .../src/main/AndroidManifest.xml | 4 +--- 2 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 dexmaker-mockito-inline-extended-tests/src/androidTest/AndroidManifest.xml diff --git a/dexmaker-mockito-inline-extended-tests/src/androidTest/AndroidManifest.xml b/dexmaker-mockito-inline-extended-tests/src/androidTest/AndroidManifest.xml new file mode 100644 index 0000000..ec08df4 --- /dev/null +++ b/dexmaker-mockito-inline-extended-tests/src/androidTest/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + + + + diff --git a/dexmaker-mockito-inline-extended-tests/src/main/AndroidManifest.xml b/dexmaker-mockito-inline-extended-tests/src/main/AndroidManifest.xml index d9e9b95..a5ee3a1 100644 --- a/dexmaker-mockito-inline-extended-tests/src/main/AndroidManifest.xml +++ b/dexmaker-mockito-inline-extended-tests/src/main/AndroidManifest.xml @@ -3,7 +3,5 @@ xmlns:tools="http://schemas.android.com/tools"> - - + tools:ignore="HardcodedDebugMode" />