Skip to content

Commit

Permalink
Merge pull request #2613 from mockito/graal-support
Browse files Browse the repository at this point in the history
Support subclass mocks on Graal VM.
  • Loading branch information
raphw committed Apr 19, 2022
2 parents 8314824 + d23dc0e commit 2c0bf94
Show file tree
Hide file tree
Showing 6 changed files with 117 additions and 37 deletions.
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()
? 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

0 comments on commit 2c0bf94

Please sign in to comment.