Skip to content

Commit bfab743

Browse files
authored
Fall back to Throwable Location strategy on Android (#3619)
Works around an Android Runtime bug where StackWalker would fail to traverse the stack if a stack frame involves java.lang.Proxy. Disables StackWalker for Android versions that do not yet include the fix in https://r.android.com/3548340. This issue was also reported as linkedin/dexmaker#190. Fixes #3171
1 parent 4f469c8 commit bfab743

3 files changed

Lines changed: 64 additions & 0 deletions

File tree

mockito-core/src/main/java/org/mockito/internal/debugging/LocationFactory.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
*/
55
package org.mockito.internal.debugging;
66

7+
import org.mockito.internal.util.AndroidPlatform;
8+
import org.mockito.internal.util.Platform;
79
import org.mockito.invocation.Location;
810

911
public final class LocationFactory {
@@ -24,6 +26,9 @@ private interface Factory {
2426
}
2527

2628
private static Factory createLocationFactory() {
29+
if (Platform.isAndroid() && !AndroidPlatform.isStackWalkerUsable()) {
30+
return new Java8LocationFactory();
31+
}
2732
try {
2833
// On some platforms, like Android, the StackWalker APIs may not be
2934
// available, in this case we have to fallback to Java 8 style of stack
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright (c) 2025 Mockito contributors
3+
* This program is made available under the terms of the MIT License.
4+
*/
5+
package org.mockito.internal.util;
6+
7+
public class AndroidPlatform {
8+
9+
private static int getSdkInt() {
10+
try {
11+
return Class.forName("android.os.Build$VERSION").getField("SDK_INT").getInt(null);
12+
} catch (ReflectiveOperationException e) {
13+
return 0;
14+
}
15+
}
16+
17+
private static int getExtensionVersion(int sdk) {
18+
try {
19+
return (int)
20+
Class.forName("android.os.ext.SdkExtensions")
21+
.getMethod("getExtensionVersion", int.class)
22+
.invoke(null, sdk);
23+
} catch (ReflectiveOperationException e) {
24+
return 0;
25+
}
26+
}
27+
28+
public static boolean isStackWalkerUsable() {
29+
// StackWalker on Android had a bug that is fixed in Android Baklava (36)
30+
// or SDK extension train M2025-05 (17) and later. See https://r.android.com/3548340.
31+
return getSdkInt() >= 36 || getSdkInt() >= 31 && getExtensionVersion(31) >= 17;
32+
}
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package org.mockitousage.androidtest
2+
3+
import androidx.test.ext.junit.runners.AndroidJUnit4
4+
import org.junit.Test
5+
import org.junit.runner.RunWith
6+
import org.mockito.Mockito.mock
7+
import org.mockito.Mockito.withSettings
8+
import org.mockito.internal.creation.proxy.ProxyMockMaker
9+
10+
@RunWith(AndroidJUnit4::class)
11+
class AaaMustRunFirstLocationTests {
12+
13+
/**
14+
* Regression test for https://github.com/mockito/mockito/issues/3171 and
15+
* https://github.com/linkedin/dexmaker/issues/190.
16+
*
17+
* Bug only triggers if the first time LocationImpl is used is with ProxyMockMaker, therefore
18+
* this test must be the first to run.
19+
*/
20+
@Test
21+
fun mockAndUseInterfaceWithProxyMockMaker() {
22+
val basicInterface = mock(BasicInterface::class.java,
23+
withSettings().mockMaker(ProxyMockMaker::class.java.name))
24+
basicInterface.interfaceMethod()
25+
}
26+
}

0 commit comments

Comments
 (0)