Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support subclass mocks on Graal VM. #2613

Merged
merged 1 commit into from Apr 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -33,6 +33,7 @@
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.method.MethodList;
import net.bytebuddy.description.method.ParameterDescription;
import net.bytebuddy.description.type.TypeDefinition;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.scaffold.MethodGraph;
import net.bytebuddy.implementation.Implementation;
Expand Down Expand Up @@ -188,7 +189,9 @@ public boolean isOverridden(Object instance, Method origin) {
SoftReference<MethodGraph> reference = graphs.get(instance.getClass());
MethodGraph methodGraph = reference == null ? null : reference.get();
if (methodGraph == null) {
methodGraph = compiler.compile(new TypeDescription.ForLoadedType(instance.getClass()));
methodGraph =
compiler.compile(
(TypeDefinition) TypeDescription.ForLoadedType.of(instance.getClass()));
graphs.put(instance.getClass(), new SoftReference<>(methodGraph));
}
MethodGraph.Node node =
Expand Down
Expand Up @@ -4,23 +4,25 @@
*/
package org.mockito.internal.creation.bytebuddy;

import static net.bytebuddy.matcher.ElementMatchers.isTypeInitializer;
import static org.mockito.internal.util.StringUtil.join;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Random;

import net.bytebuddy.ByteBuddy;
import net.bytebuddy.description.modifier.Ownership;
import net.bytebuddy.description.modifier.Visibility;
import net.bytebuddy.dynamic.scaffold.subclass.ConstructorStrategy;
import net.bytebuddy.implementation.Implementation;
import net.bytebuddy.implementation.MethodCall;
import net.bytebuddy.implementation.StubMethod;
import net.bytebuddy.utility.GraalImageCode;
import net.bytebuddy.utility.RandomString;
import org.mockito.Mockito;
import org.mockito.codegen.InjectionBase;
import org.mockito.exceptions.base.MockitoException;

import java.lang.reflect.Field;
import java.lang.reflect.Method;

import static net.bytebuddy.matcher.ElementMatchers.isTypeInitializer;
import static org.mockito.internal.util.StringUtil.join;

abstract class ModuleHandler {

abstract boolean isOpened(Class<?> source, Class<?> target);
Expand All @@ -35,9 +37,9 @@ abstract class ModuleHandler {

abstract void adjustModuleGraph(Class<?> source, Class<?> target, boolean export, boolean read);

static ModuleHandler make(ByteBuddy byteBuddy, SubclassLoader loader, Random random) {
static ModuleHandler make(ByteBuddy byteBuddy, SubclassLoader loader) {
try {
return new ModuleSystemFound(byteBuddy, loader, random);
return new ModuleSystemFound(byteBuddy, loader);
} catch (Exception ignored) {
return new NoModuleSystemFound();
}
Expand All @@ -47,7 +49,6 @@ private static class ModuleSystemFound extends ModuleHandler {

private final ByteBuddy byteBuddy;
private final SubclassLoader loader;
private final Random random;

private final int injectonBaseSuffix;

Expand All @@ -58,15 +59,15 @@ private static class ModuleSystemFound extends ModuleHandler {
canRead,
addExports,
addReads,
addOpens,
forName;

private ModuleSystemFound(ByteBuddy byteBuddy, SubclassLoader loader, Random random)
throws Exception {
private ModuleSystemFound(ByteBuddy byteBuddy, SubclassLoader loader) throws Exception {
this.byteBuddy = byteBuddy;
this.loader = loader;
this.random = random;
injectonBaseSuffix = Math.abs(random.nextInt());
injectonBaseSuffix =
GraalImageCode.getCurrent().isDefined()
? 0
: Math.abs(Mockito.class.hashCode());
Class<?> moduleType = Class.forName("java.lang.Module");
getModule = Class.class.getMethod("getModule");
isOpen = moduleType.getMethod("isOpen", String.class, moduleType);
Expand All @@ -75,7 +76,6 @@ private ModuleSystemFound(ByteBuddy byteBuddy, SubclassLoader loader, Random ran
canRead = moduleType.getMethod("canRead", moduleType);
addExports = moduleType.getMethod("addExports", String.class, moduleType);
addReads = moduleType.getMethod("addReads", moduleType);
addOpens = moduleType.getMethod("addOpens", String.class, moduleType);
forName = Class.class.getMethod("forName", String.class);
}

Expand Down Expand Up @@ -207,9 +207,12 @@ void adjustModuleGraph(Class<?> source, Class<?> target, boolean export, boolean
ConstructorStrategy.Default.NO_CONSTRUCTORS)
.name(
String.format(
"%s$%d",
"%s$%s%s",
"org.mockito.codegen.MockitoTypeCarrier",
Math.abs(random.nextInt())))
RandomString.hashOf(
source.getName().hashCode()),
RandomString.hashOf(
target.getName().hashCode())))
.defineField(
"mockitoType",
Class.class,
Expand Down Expand Up @@ -262,10 +265,11 @@ void adjustModuleGraph(Class<?> source, Class<?> target, boolean export, boolean
.subclass(Object.class)
.name(
String.format(
"%s$%s$%d",
"%s$%s$%s%s",
source.getName(),
"MockitoModuleProbe",
Math.abs(random.nextInt())))
RandomString.hashOf(source.getName().hashCode()),
RandomString.hashOf(target.getName().hashCode())))
.invokable(isTypeInitializer())
.intercept(implementation)
.make()
Expand Down
Expand Up @@ -21,13 +21,13 @@

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Random;
import java.util.*;

import net.bytebuddy.ByteBuddy;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.modifier.SynchronizationState;
Expand All @@ -39,6 +39,8 @@
import net.bytebuddy.implementation.Implementation;
import net.bytebuddy.implementation.attribute.MethodAttributeAppender;
import net.bytebuddy.matcher.ElementMatcher;
import net.bytebuddy.utility.GraalImageCode;
import net.bytebuddy.utility.RandomString;
import org.mockito.codegen.InjectionBase;
import org.mockito.exceptions.base.MockitoException;
import org.mockito.internal.creation.bytebuddy.ByteBuddyCrossClassLoaderSerializationSupport.CrossClassLoaderSerializableMock;
Expand All @@ -52,7 +54,6 @@ class SubclassBytecodeGenerator implements BytecodeGenerator {
private final SubclassLoader loader;
private final ModuleHandler handler;
private final ByteBuddy byteBuddy;
private final Random random;
private final Implementation readReplace;
private final ElementMatcher<? super MethodDescription> matcher;

Expand Down Expand Up @@ -82,8 +83,7 @@ protected SubclassBytecodeGenerator(
this.readReplace = readReplace;
this.matcher = matcher;
byteBuddy = new ByteBuddy().with(TypeValidation.DISABLED);
random = new Random();
handler = ModuleHandler.make(byteBuddy, loader, random);
handler = ModuleHandler.make(byteBuddy, loader);
}

private static boolean needsSamePackageClassLoader(MockFeatures<?> features) {
Expand Down Expand Up @@ -167,7 +167,8 @@ public <T> Class<? extends T> mockClass(MockFeatures<T> features) {
&& features.serializableMode != SerializableMode.ACROSS_CLASSLOADERS
&& !isComingFromJDK(features.mockedType)
&& (loader.isDisrespectingOpenness()
|| handler.isOpened(features.mockedType, MockAccess.class));
|| handler.isOpened(features.mockedType, MockAccess.class))
&& !GraalImageCode.getCurrent().isDefined();
String typeName;
if (localMock
|| (loader instanceof MultipleParentClassLoader
Expand All @@ -180,7 +181,13 @@ public <T> Class<? extends T> mockClass(MockFeatures<T> features) {
+ features.mockedType.getSimpleName();
}
String name =
String.format("%s$%s$%d", typeName, "MockitoMock", Math.abs(random.nextInt()));
String.format(
"%s$%s$%s",
typeName,
"MockitoMock",
GraalImageCode.getCurrent().isDefined()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a specific reason we only want this on Graal? I can imagine we would want this for others as well, to keep everything consistent?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The very theoretical situation would be a OSGi setup where multiple modules of Mockito wanted to create a mock for the same class loader. It's unlikely but might trigger a regression. The question is if the stable name justifies this theoretical issue.

? suffix(features)
: RandomString.make());

if (localMock) {
handler.adjustModuleGraph(features.mockedType, MockAccess.class, false, true);
Expand Down Expand Up @@ -214,17 +221,36 @@ public <T> Class<? extends T> mockClass(MockFeatures<T> features) {
}
}
}

// Graal requires that the byte code of classes is identical what requires that interfaces
// are always
// defined in the exact same order. Therefore, we add an interface to the interface set if
// not mocking
// a class when Graal is active.
@SuppressWarnings("unchecked")
Class<T> target =
GraalImageCode.getCurrent().isDefined() && features.mockedType.isInterface()
? (Class<T>) Object.class
: features.mockedType;
DynamicType.Builder<T> builder =
byteBuddy
.subclass(features.mockedType)
.subclass(target)
.name(name)
.ignoreAlso(BytecodeGenerator.isGroovyMethod(false))
.annotateType(
features.stripAnnotations
features.stripAnnotations || features.mockedType.isInterface()
? new Annotation[0]
: features.mockedType.getAnnotations())
.implement(new ArrayList<Type>(features.interfaces))
.implement(
new ArrayList<>(
GraalImageCode.getCurrent().isDefined()
? sortedSerializable(
features.interfaces,
GraalImageCode.getCurrent().isDefined()
&& features.mockedType
.isInterface()
? features.mockedType
: void.class)
: features.interfaces))
.method(matcher)
.intercept(dispatcher)
.transform(withModifiers(SynchronizationState.PLAIN))
Expand Down Expand Up @@ -266,6 +292,31 @@ public <T> Class<? extends T> mockClass(MockFeatures<T> features) {
.getLoaded();
}

private static CharSequence suffix(MockFeatures<?> features) {
// Constructs a deterministic suffix for this mock to assure that mocks always carry the
// same name.
StringBuilder sb = new StringBuilder();
Set<String> names = new TreeSet<>();
names.add(features.mockedType.getName());
for (Class<?> type : features.interfaces) {
names.add(type.getName());
}
return sb.append(RandomString.hashOf(names.hashCode()))
.append(RandomString.hashOf(features.serializableMode.name().hashCode()))
.append(features.stripAnnotations ? "S" : "N");
}

private static Collection<? extends Type> sortedSerializable(
Collection<Class<?>> interfaces, Class<?> mockedType) {
SortedSet<Class<?>> types = new TreeSet<>(Comparator.comparing(Class::getName));
types.addAll(interfaces);
if (mockedType != void.class) {
types.add(mockedType);
}
types.add(Serializable.class);
return types;
}

@Override
public void mockClassStatic(Class<?> type) {
throw new MockitoException("The subclass byte code generator cannot create static mocks");
Expand Down
Expand Up @@ -11,6 +11,7 @@

import net.bytebuddy.dynamic.loading.ClassInjector;
import net.bytebuddy.dynamic.loading.ClassLoadingStrategy;
import net.bytebuddy.utility.GraalImageCode;
import org.mockito.codegen.InjectionBase;
import org.mockito.exceptions.base.MockitoException;
import org.mockito.internal.util.Platform;
Expand All @@ -27,9 +28,14 @@ class SubclassInjectionLoader implements SubclassLoader {
private final SubclassLoader loader;

SubclassInjectionLoader() {
if (!Boolean.getBoolean("org.mockito.internal.noUnsafeInjection")
if (!Boolean.parseBoolean(
System.getProperty(
"org.mockito.internal.noUnsafeInjection",
Boolean.toString(GraalImageCode.getCurrent().isDefined())))
&& ClassInjector.UsingReflection.isAvailable()) {
this.loader = new WithReflection();
} else if (GraalImageCode.getCurrent().isDefined()) {
this.loader = new WithIsolatedLoader();
} else if (ClassInjector.UsingLookup.isAvailable()) {
this.loader = tryLookup();
} else {
Expand Down Expand Up @@ -70,6 +76,20 @@ public ClassLoadingStrategy<ClassLoader> resolveStrategy(
}
}

private static class WithIsolatedLoader implements SubclassLoader {

@Override
public boolean isDisrespectingOpenness() {
return false;
}

@Override
public ClassLoadingStrategy<ClassLoader> resolveStrategy(
Class<?> mockedType, ClassLoader classLoader, boolean localMock) {
return ClassLoadingStrategy.Default.WRAPPER;
}
}

private static class WithLookup implements SubclassLoader {

private final Object lookup;
Expand Down
6 changes: 5 additions & 1 deletion src/main/java/org/mockito/internal/util/Platform.java
Expand Up @@ -69,7 +69,11 @@ public static String describe() {
}

public static boolean isJava8BelowUpdate45() {
return isJava8BelowUpdate45(JVM_VERSION);
if (JVM_VERSION == null) {
return false;
} else {
return isJava8BelowUpdate45(JVM_VERSION);
}
}

static boolean isJava8BelowUpdate45(String jvmVersion) {
Expand Down
Expand Up @@ -15,8 +15,6 @@

public class NoByteCodeDependenciesTest {

private ClassLoader contextClassLoader;

@Test
public void pure_mockito_should_not_depend_bytecode_libraries() throws Exception {

Expand Down