Skip to content

Commit

Permalink
Support subclass mocks on Graal VM.
Browse files Browse the repository at this point in the history
  • Loading branch information
raphw committed Apr 7, 2022
1 parent 6ccc121 commit 6270a9d
Show file tree
Hide file tree
Showing 7 changed files with 100 additions and 30 deletions.
2 changes: 1 addition & 1 deletion gradle/dependencies.gradle
Expand Up @@ -4,7 +4,7 @@ ext {

def versions = [:]

versions.bytebuddy = '1.12.8'
versions.bytebuddy = '1.12.9'
versions.junitJupiter = '5.8.2'
versions.errorprone = '2.10.0'

Expand Down
Expand Up @@ -9,7 +9,6 @@

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

import net.bytebuddy.ByteBuddy;
import net.bytebuddy.description.modifier.Ownership;
Expand All @@ -18,6 +17,8 @@
import net.bytebuddy.implementation.Implementation;
import net.bytebuddy.implementation.MethodCall;
import net.bytebuddy.implementation.StubMethod;
import net.bytebuddy.utility.RandomString;
import org.mockito.Mockito;
import org.mockito.codegen.InjectionBase;
import org.mockito.exceptions.base.MockitoException;

Expand All @@ -35,9 +36,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 +48,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 @@ -61,12 +61,10 @@ private static class ModuleSystemFound extends ModuleHandler {
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 = 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 Down Expand Up @@ -207,9 +205,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 +263,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 @@ -24,15 +24,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.Collection;
import java.util.Iterator;
import java.util.LinkedList;
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 @@ -44,6 +42,9 @@
import net.bytebuddy.implementation.Implementation;
import net.bytebuddy.implementation.attribute.MethodAttributeAppender;
import net.bytebuddy.matcher.ElementMatcher;
import net.bytebuddy.utility.CompoundList;
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 @@ -57,7 +58,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 @@ -87,8 +87,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 @@ -172,7 +171,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 @@ -185,7 +185,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 @@ -229,7 +235,13 @@ public <T> Class<? extends T> mockClass(MockFeatures<T> features) {
features.stripAnnotations
? new Annotation[0]
: features.mockedType.getAnnotations())
.implement(new ArrayList<Type>(features.interfaces))
.implement(
GraalImageCode.getCurrent().isDefined()
&& !features.interfaces.contains(Serializable.class)
? CompoundList.of(
new ArrayList<Type>(features.interfaces),
Serializable.class)
: new ArrayList<Type>(features.interfaces))
.method(matcher)
.intercept(dispatcher)
.transform(withModifiers(SynchronizationState.PLAIN))
Expand Down Expand Up @@ -271,6 +283,20 @@ 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");
}

@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
Expand Up @@ -4,21 +4,39 @@
*/
package org.mockito.internal.creation.instance;

import net.bytebuddy.utility.GraalImageCode;
import org.mockito.creation.instance.Instantiator;
import org.mockito.internal.configuration.GlobalConfiguration;
import org.objenesis.Objenesis;
import org.objenesis.ObjenesisBase;
import org.objenesis.ObjenesisStd;
import org.objenesis.instantiator.ObjectInstantiator;
import org.objenesis.instantiator.sun.UnsafeFactoryInstantiator;
import org.objenesis.strategy.InstantiatorStrategy;

class ObjenesisInstantiator implements Instantiator {

// TODO: in order to provide decent exception message when objenesis is not found,
// have a constructor in this class that tries to instantiate ObjenesisStd and if it fails then
// show decent exception that dependency is missing
// TODO: for the same reason catch and give better feedback when hamcrest core is not found.
private final ObjenesisStd objenesis =
new ObjenesisStd(new GlobalConfiguration().enableClassCache());
private final Objenesis objenesis =
GraalImageCode.getCurrent().isDefined()
? new ObjenesisBase(new UnsafeInstantiatorStrategy(), true)
: new ObjenesisStd(new GlobalConfiguration().enableClassCache());

@Override
public <T> T newInstance(Class<T> cls) {
return objenesis.newInstance(cls);
}

private static class UnsafeInstantiatorStrategy implements InstantiatorStrategy {

@Override
@SuppressWarnings("CheckReturnValue")
public <T> ObjectInstantiator<T> newInstantiatorOf(Class<T> type) {
type.getDeclaredConstructors(); // Graal does not track Unsafe constructors.
return new UnsafeFactoryInstantiator<>(type);
}
}
}
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 All @@ -35,6 +33,8 @@ public void pure_mockito_should_not_depend_bytecode_libraries() throws Exception
"org.mockito.internal.creation.instance.DefaultInstantiatorProvider");
pureMockitoAPIClasses.remove(
"org.mockito.internal.creation.instance.ObjenesisInstantiator");
pureMockitoAPIClasses.remove(
"org.mockito.internal.creation.instance.ObjenesisInstantiator$UnsafeInstantiatorStrategy");

// Remove classes that trigger plugin-loading, since bytebuddy plugins are the default.
pureMockitoAPIClasses.remove("org.mockito.internal.debugging.LocationImpl");
Expand Down

0 comments on commit 6270a9d

Please sign in to comment.