From f28941a0adecc01c8ba39388c84b33d5607aaa06 Mon Sep 17 00:00:00 2001 From: Rafael Winterhalter Date: Sat, 11 Jun 2022 23:02:35 +0200 Subject: [PATCH] Avoids starting mocks "half-way" if a superclass constructor is mocked but an unmocked subclass is initiated. --- .../InlineDelegateByteBuddyMockMaker.java | 6 ++ .../creation/bytebuddy/StackTraceChecker.java | 36 ++++++++ .../bytebuddy/StackWalkerChecker.java | 91 +++++++++++++++++++ .../mockitoinline/SubconstructorMockTest.java | 43 +++++++++ 4 files changed, 176 insertions(+) create mode 100644 src/main/java/org/mockito/internal/creation/bytebuddy/StackTraceChecker.java create mode 100644 src/main/java/org/mockito/internal/creation/bytebuddy/StackWalkerChecker.java create mode 100644 subprojects/inline/src/test/java/org/mockitoinline/SubconstructorMockTest.java diff --git a/src/main/java/org/mockito/internal/creation/bytebuddy/InlineDelegateByteBuddyMockMaker.java b/src/main/java/org/mockito/internal/creation/bytebuddy/InlineDelegateByteBuddyMockMaker.java index bf6cf4b735..6fcb6a500e 100644 --- a/src/main/java/org/mockito/internal/creation/bytebuddy/InlineDelegateByteBuddyMockMaker.java +++ b/src/main/java/org/mockito/internal/creation/bytebuddy/InlineDelegateByteBuddyMockMaker.java @@ -251,6 +251,7 @@ class InlineDelegateByteBuddyMockMaker ThreadLocal> currentConstruction = new ThreadLocal<>(); ThreadLocal isSuspended = ThreadLocal.withInitial(() -> false); + Predicate> isCallFromSubclassConstructor = StackWalkerChecker.orFallback(); Predicate> isMockConstruction = type -> { if (isSuspended.get()) { @@ -260,6 +261,11 @@ class InlineDelegateByteBuddyMockMaker } Map, ?> interceptors = mockedConstruction.get(); if (interceptors != null && interceptors.containsKey(type)) { + // We only initiate a construction mock, if the call originates from an + // un-mocked (as suppression is not enabled) subclass constructor. + if (isCallFromSubclassConstructor.test(type)) { + return false; + } currentConstruction.set(type); return true; } else { diff --git a/src/main/java/org/mockito/internal/creation/bytebuddy/StackTraceChecker.java b/src/main/java/org/mockito/internal/creation/bytebuddy/StackTraceChecker.java new file mode 100644 index 0000000000..18f92ba690 --- /dev/null +++ b/src/main/java/org/mockito/internal/creation/bytebuddy/StackTraceChecker.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2022 Mockito contributors + * This program is made available under the terms of the MIT License. + */ +package org.mockito.internal.creation.bytebuddy; + +import java.util.function.Predicate; + +class StackTraceChecker implements Predicate> { + + @Override + public boolean test(Class type) { + StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); + for (int index = 1; index < stackTrace.length - 1; index++) { + if (!stackTrace[index].getClassName().startsWith("org.mockito.internal.")) { + if (stackTrace[index + 1].getMethodName().startsWith("")) { + try { + if (!stackTrace[index + 1].getClassName().equals(type.getName()) + && type.isAssignableFrom( + Class.forName( + stackTrace[index + 1].getClassName(), + false, + type.getClassLoader()))) { + return true; + } else { + break; + } + } catch (ClassNotFoundException ignored) { + break; + } + } + } + } + return false; + } +} diff --git a/src/main/java/org/mockito/internal/creation/bytebuddy/StackWalkerChecker.java b/src/main/java/org/mockito/internal/creation/bytebuddy/StackWalkerChecker.java new file mode 100644 index 0000000000..c16439dbcb --- /dev/null +++ b/src/main/java/org/mockito/internal/creation/bytebuddy/StackWalkerChecker.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2022 Mockito contributors + * This program is made available under the terms of the MIT License. + */ +package org.mockito.internal.creation.bytebuddy; + +import java.lang.reflect.Method; +import java.util.Iterator; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Stream; + +class StackWalkerChecker implements Predicate> { + + private final Method stackWalkerGetInstance; + private final Method stackWalkerWalk; + private final Method stackWalkerStackFrameGetDeclaringClass; + private final Enum stackWalkerOptionRetainClassReference; + + StackWalkerChecker() throws Exception { + Class stackWalker = Class.forName("java.lang.StackWalker"); + @SuppressWarnings({"unchecked", "rawtypes"}) + Class stackWalkerOption = + (Class>) Class.forName("java.lang.StackWalker$Option"); + stackWalkerGetInstance = stackWalker.getMethod("getInstance", stackWalkerOption); + stackWalkerWalk = stackWalker.getMethod("walk", Function.class); + Class stackWalkerStackFrame = Class.forName("java.lang.StackWalker$StackFrame"); + stackWalkerStackFrameGetDeclaringClass = + stackWalkerStackFrame.getMethod("getDeclaringClass"); + @SuppressWarnings("unchecked") + Enum stackWalkerOptionRetainClassReference = + Enum.valueOf(stackWalkerOption, "RETAIN_CLASS_REFERENCE"); + this.stackWalkerOptionRetainClassReference = stackWalkerOptionRetainClassReference; + } + + static Predicate> orFallback() { + try { + return new StackWalkerChecker(); + } catch (Exception e) { + return new StackTraceChecker(); + } + } + + @Override + public boolean test(Class type) { + try { + Object walker = + stackWalkerGetInstance.invoke(null, stackWalkerOptionRetainClassReference); + return (Boolean) + stackWalkerWalk.invoke( + walker, + (Function) + stream -> { + Iterator iterator = ((Stream) stream).iterator(); + while (iterator.hasNext()) { + try { + Object frame = iterator.next(); + if (((Class) + stackWalkerStackFrameGetDeclaringClass + .invoke(frame)) + .getName() + .startsWith("org.mockito.internal.")) { + continue; + } + if (iterator.hasNext()) { + Object next = iterator.next(); + Class declaringClass = + (Class) + stackWalkerStackFrameGetDeclaringClass + .invoke(next); + if (type != declaringClass + && type.isAssignableFrom( + declaringClass)) { + return true; + } else { + break; + } + } else { + break; + } + } catch (Exception ignored) { + return false; + } + } + return false; + }); + } catch (Exception ignored) { + return false; + } + } +} diff --git a/subprojects/inline/src/test/java/org/mockitoinline/SubconstructorMockTest.java b/subprojects/inline/src/test/java/org/mockitoinline/SubconstructorMockTest.java new file mode 100644 index 0000000000..bbf95eb5f5 --- /dev/null +++ b/subprojects/inline/src/test/java/org/mockitoinline/SubconstructorMockTest.java @@ -0,0 +1,43 @@ +package org.mockitoinline; + +import org.junit.Test; +import org.mockito.MockedConstruction; +import org.mockito.Mockito; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class SubconstructorMockTest { + + @Test + public void does_not_mock_subclass_constructor_for_superclass_mock() { + try (MockedConstruction mocked = Mockito.mockConstruction(SubClass.class)) { } + try (MockedConstruction mocked = Mockito.mockConstruction(SuperClass.class)) { + SubClass value = new SubClass(); + assertTrue(value.sup()); + assertTrue(value.sub()); + } + } + + @Test + public void does_mock_superclass_constructor_for_subclass_mock() { + try (MockedConstruction mocked = Mockito.mockConstruction(SuperClass.class)) { } + try (MockedConstruction mocked = Mockito.mockConstruction(SubClass.class)) { + SubClass value = new SubClass(); + assertFalse(value.sup()); + assertFalse(value.sub()); + } + } + + public static class SuperClass { + public boolean sup() { + return true; + } + } + + public static class SubClass extends SuperClass { + public boolean sub() { + return true; + } + } +}