Skip to content

Commit

Permalink
Add checks for sealed types
Browse files Browse the repository at this point in the history
  • Loading branch information
raphw committed Aug 17, 2021
1 parent 28020c0 commit ca6f3ef
Show file tree
Hide file tree
Showing 4 changed files with 87 additions and 2 deletions.
Expand Up @@ -344,7 +344,9 @@ private <T> void checkSupportedCombination(
if (subclassingRequired
&& !features.mockedType.isArray()
&& !features.mockedType.isPrimitive()
&& Modifier.isFinal(features.mockedType.getModifiers())) {
&& (Modifier.isFinal(features.mockedType.getModifiers())
|| TypeSupport.INSTANCE.isSealed(features.mockedType)
|| features.interfaces.stream().anyMatch(TypeSupport.INSTANCE::isSealed))) {
throw new MockitoException(
"Unsupported settings with this type '" + features.mockedType.getName() + "'");
}
Expand Down
Expand Up @@ -162,7 +162,9 @@ public TypeMockability isTypeMockable(final Class<?> type) {
return new TypeMockability() {
@Override
public boolean mockable() {
return !type.isPrimitive() && !Modifier.isFinal(type.getModifiers());
return !type.isPrimitive()
&& !Modifier.isFinal(type.getModifiers())
&& !TypeSupport.INSTANCE.isSealed(type);
}

@Override
Expand All @@ -176,6 +178,9 @@ public String nonMockableReason() {
if (Modifier.isFinal(type.getModifiers())) {
return "final class";
}
if (TypeSupport.INSTANCE.isSealed(type)) {
return "sealed class";
}
return join("not handled type");
}
};
Expand Down
@@ -0,0 +1,49 @@
/*
* Copyright (c) 2021 Mockito contributors
* This program is made available under the terms of the MIT License.
*/
package org.mockito.internal.creation.bytebuddy;

import org.mockito.exceptions.base.MockitoException;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;

class TypeSupport {

static final TypeSupport INSTANCE;

static {
MethodHandle isSealed;
try {
isSealed =
MethodHandles.publicLookup()
.findVirtual(
Class.class, "isSealed", MethodType.methodType(boolean.class));
} catch (NoSuchMethodException e) {
isSealed = null;
} catch (IllegalAccessException e) {
throw new MockitoException("Failed to access Class.isSealed", e);
}
INSTANCE = new TypeSupport(isSealed);
}

private final MethodHandle isSealed;

private TypeSupport(MethodHandle isSealed) {
this.isSealed = isSealed;
}

boolean isSealed(Class<?> type) {
if (isSealed == null) {
return false;
}
try {
return (boolean) isSealed.invoke(type);
} catch (Throwable t) {
throw new MockitoException(
"Failed to check if type is sealed using handle " + isSealed, t);
}
}
}
Expand Up @@ -11,6 +11,10 @@
import java.util.Observable;
import java.util.Observer;

import net.bytebuddy.ByteBuddy;
import net.bytebuddy.ClassFileVersion;
import net.bytebuddy.description.modifier.TypeManifestation;
import net.bytebuddy.dynamic.DynamicType;
import org.junit.Test;
import org.mockito.internal.creation.MockSettingsImpl;
import org.mockito.plugins.MockMaker;
Expand All @@ -29,6 +33,31 @@ public void is_type_mockable_excludes_primitive_wrapper_classes() {
assertThat(mockable.nonMockableReason()).contains("final");
}

@Test
public void is_type_mockable_excludes_sealed_classes() {
// is only supported on Java 17 and later
if (ClassFileVersion.ofThisVm().isAtMost(ClassFileVersion.JAVA_V16)) {
return;
}
DynamicType.Builder<Object> base = new ByteBuddy().subclass(Object.class);
DynamicType.Unloaded<Object> dynamic =
new ByteBuddy()
.subclass(Object.class)
.permittedSubclass(base.toTypeDescription())
.make();
Class<?> type =
new ByteBuddy()
.subclass(base.toTypeDescription())
.merge(TypeManifestation.FINAL)
.make()
.include(dynamic)
.load(null)
.getLoaded();
MockMaker.TypeMockability mockable = mockMaker.isTypeMockable(type);
assertThat(mockable.mockable()).isFalse();
assertThat(mockable.nonMockableReason()).contains("sealed");
}

@Test
public void is_type_mockable_excludes_primitive_classes() {
MockMaker.TypeMockability mockable = mockMaker.isTypeMockable(int.class);
Expand Down

0 comments on commit ca6f3ef

Please sign in to comment.