From bebf746e804899437c71533727cb4531284e3574 Mon Sep 17 00:00:00 2001 From: Stephane Epardaud Date: Wed, 25 Mar 2020 16:44:27 +0100 Subject: [PATCH] New panache-mock module for mocking Panache static methods --- bom/deployment/pom.xml | 5 + bom/runtime/pom.xml | 5 + .../builditem/FeatureBuildItem.java | 1 + .../deployment/PanacheJpaEntityEnhancer.java | 13 +- .../deployment/PanacheResourceProcessor.java | 11 +- .../hibernate-orm-panache/runtime/pom.xml | 10 + .../panache/common/deployment/JandexUtil.java | 232 ++++++++++++++++++ .../deployment/PanacheEntityEnhancer.java | 26 +- .../deployment/PanacheMethodCustomizer.java | 11 + .../PanacheMethodCustomizerBuildItem.java | 18 ++ .../PanacheMethodCustomizerVisitor.java | 31 +++ .../panache/panache-mock/deployment/pom.xml | 59 +++++ .../PanacheMockMethodCustomizer.java | 123 ++++++++++ .../PanacheMockResourceProcessor.java | 17 ++ extensions/panache/panache-mock/pom.xml | 22 ++ .../panache/panache-mock/runtime/pom.xml | 38 +++ .../io/quarkus/panache/mock/PanacheMock.java | 81 ++++++ .../resources/META-INF/quarkus-extension.yaml | 15 ++ extensions/panache/pom.xml | 1 + .../hibernate-orm-panache/pom.xml | 13 + .../it/panache/PanacheFunctionalityTest.java | 43 ++++ 21 files changed, 766 insertions(+), 9 deletions(-) create mode 100644 extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheMethodCustomizer.java create mode 100644 extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheMethodCustomizerBuildItem.java create mode 100644 extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheMethodCustomizerVisitor.java create mode 100644 extensions/panache/panache-mock/deployment/pom.xml create mode 100644 extensions/panache/panache-mock/deployment/src/main/java/io/quarkus/panache/mock/deployment/PanacheMockMethodCustomizer.java create mode 100644 extensions/panache/panache-mock/deployment/src/main/java/io/quarkus/panache/mock/deployment/PanacheMockResourceProcessor.java create mode 100644 extensions/panache/panache-mock/pom.xml create mode 100644 extensions/panache/panache-mock/runtime/pom.xml create mode 100644 extensions/panache/panache-mock/runtime/src/main/java/io/quarkus/panache/mock/PanacheMock.java create mode 100644 extensions/panache/panache-mock/runtime/src/main/resources/META-INF/quarkus-extension.yaml diff --git a/bom/deployment/pom.xml b/bom/deployment/pom.xml index 9062988bd45927..8b1d7ab6313f77 100644 --- a/bom/deployment/pom.xml +++ b/bom/deployment/pom.xml @@ -351,6 +351,11 @@ quarkus-panache-common-deployment ${project.version} + + io.quarkus + quarkus-panache-mock-deployment + ${project.version} + io.quarkus quarkus-mongodb-panache-deployment diff --git a/bom/runtime/pom.xml b/bom/runtime/pom.xml index 11c8f780374f30..0018729e46e600 100644 --- a/bom/runtime/pom.xml +++ b/bom/runtime/pom.xml @@ -409,6 +409,11 @@ quarkus-panache-common ${project.version} + + io.quarkus + quarkus-panache-mock + ${project.version} + io.quarkus quarkus-panacheql diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/FeatureBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/FeatureBuildItem.java index 92da1114320c80..346aa211f25796 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/builditem/FeatureBuildItem.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/FeatureBuildItem.java @@ -54,6 +54,7 @@ public final class FeatureBuildItem extends MultiBuildItem { public static final String OIDC = "oidc"; public static final String OPTAPLANNER = "optaplanner"; public static final String OPTAPLANNER_JACKSON = "optaplanner-jackson"; + public static final String PANACHE_MOCK = "panache-mock"; public static final String QUTE = "qute"; public static final String RESTEASY = "resteasy"; public static final String RESTEASY_JACKSON = "resteasy-jackson"; diff --git a/extensions/panache/hibernate-orm-panache/deployment/src/main/java/io/quarkus/hibernate/orm/panache/deployment/PanacheJpaEntityEnhancer.java b/extensions/panache/hibernate-orm-panache/deployment/src/main/java/io/quarkus/hibernate/orm/panache/deployment/PanacheJpaEntityEnhancer.java index f29c14ad090c2d..7e3135519826cb 100644 --- a/extensions/panache/hibernate-orm-panache/deployment/src/main/java/io/quarkus/hibernate/orm/panache/deployment/PanacheJpaEntityEnhancer.java +++ b/extensions/panache/hibernate-orm-panache/deployment/src/main/java/io/quarkus/hibernate/orm/panache/deployment/PanacheJpaEntityEnhancer.java @@ -1,6 +1,7 @@ package io.quarkus.hibernate.orm.panache.deployment; import java.lang.reflect.Modifier; +import java.util.List; import javax.persistence.Transient; @@ -22,6 +23,7 @@ import io.quarkus.panache.common.deployment.EntityModel; import io.quarkus.panache.common.deployment.MetamodelInfo; import io.quarkus.panache.common.deployment.PanacheEntityEnhancer; +import io.quarkus.panache.common.deployment.PanacheMethodCustomizer; public class PanacheJpaEntityEnhancer extends PanacheEntityEnhancer>> { @@ -38,15 +40,15 @@ public class PanacheJpaEntityEnhancer extends PanacheEntityEnhancer methodCustomizers) { + super(index, PanacheResourceProcessor.DOTNAME_PANACHE_ENTITY_BASE, methodCustomizers); modelInfo = new MetamodelInfo<>(); } @Override public ClassVisitor apply(String className, ClassVisitor outputClassVisitor) { return new PanacheJpaEntityClassVisitor(className, outputClassVisitor, modelInfo, panacheEntityBaseClassInfo, - indexView.getClassByName(DotName.createSimple(className))); + indexView.getClassByName(DotName.createSimple(className)), methodCustomizers); } static class PanacheJpaEntityClassVisitor extends PanacheEntityClassVisitor { @@ -54,8 +56,9 @@ static class PanacheJpaEntityClassVisitor extends PanacheEntityClassVisitor> modelInfo, ClassInfo panacheEntityBaseClassInfo, - ClassInfo entityInfo) { - super(className, outputClassVisitor, modelInfo, panacheEntityBaseClassInfo, entityInfo); + ClassInfo entityInfo, + List methodCustomizers) { + super(className, outputClassVisitor, modelInfo, panacheEntityBaseClassInfo, entityInfo, methodCustomizers); } @Override diff --git a/extensions/panache/hibernate-orm-panache/deployment/src/main/java/io/quarkus/hibernate/orm/panache/deployment/PanacheResourceProcessor.java b/extensions/panache/hibernate-orm-panache/deployment/src/main/java/io/quarkus/hibernate/orm/panache/deployment/PanacheResourceProcessor.java index 523a787771c6b6..123fae7e8154a4 100644 --- a/extensions/panache/hibernate-orm-panache/deployment/src/main/java/io/quarkus/hibernate/orm/panache/deployment/PanacheResourceProcessor.java +++ b/extensions/panache/hibernate-orm-panache/deployment/src/main/java/io/quarkus/hibernate/orm/panache/deployment/PanacheResourceProcessor.java @@ -4,6 +4,7 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.stream.Collectors; import javax.persistence.EntityManager; @@ -27,6 +28,8 @@ import io.quarkus.panache.common.deployment.MetamodelInfo; import io.quarkus.panache.common.deployment.PanacheEntityClassesBuildItem; import io.quarkus.panache.common.deployment.PanacheFieldAccessEnhancer; +import io.quarkus.panache.common.deployment.PanacheMethodCustomizer; +import io.quarkus.panache.common.deployment.PanacheMethodCustomizerBuildItem; import io.quarkus.panache.common.deployment.PanacheRepositoryEnhancer; public final class PanacheResourceProcessor { @@ -60,7 +63,11 @@ UnremovableBeanBuildItem ensureBeanLookupAvailable() { void build(CombinedIndexBuildItem index, BuildProducer transformers, HibernateEnhancersRegisteredBuildItem hibernateMarker, - BuildProducer entityClasses) throws Exception { + BuildProducer entityClasses, + List methodCustomizersBuildItems) throws Exception { + + List methodCustomizers = methodCustomizersBuildItems.stream() + .map(bi -> bi.getMethodCustomizer()).collect(Collectors.toList()); PanacheJpaRepositoryEnhancer daoEnhancer = new PanacheJpaRepositoryEnhancer(index.getIndex()); Set daoClasses = new HashSet<>(); @@ -81,7 +88,7 @@ void build(CombinedIndexBuildItem index, transformers.produce(new BytecodeTransformerBuildItem(daoClass, daoEnhancer)); } - PanacheJpaEntityEnhancer modelEnhancer = new PanacheJpaEntityEnhancer(index.getIndex()); + PanacheJpaEntityEnhancer modelEnhancer = new PanacheJpaEntityEnhancer(index.getIndex(), methodCustomizers); Set modelClasses = new HashSet<>(); // Note that we do this in two passes because for some reason Jandex does not give us subtypes // of PanacheEntity if we ask for subtypes of PanacheEntityBase diff --git a/extensions/panache/hibernate-orm-panache/runtime/pom.xml b/extensions/panache/hibernate-orm-panache/runtime/pom.xml index 619864a3a1296e..db189f7b4f32dd 100644 --- a/extensions/panache/hibernate-orm-panache/runtime/pom.xml +++ b/extensions/panache/hibernate-orm-panache/runtime/pom.xml @@ -39,6 +39,16 @@ quarkus-jackson true + + org.junit.jupiter + junit-jupiter + test + + + org.mockito + mockito-core + test + org.hibernate diff --git a/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/JandexUtil.java b/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/JandexUtil.java index 5a1359ed69f361..a264d4a19a2e64 100644 --- a/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/JandexUtil.java +++ b/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/JandexUtil.java @@ -2,6 +2,7 @@ import static java.util.stream.Collectors.toList; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.function.Function; @@ -13,7 +14,9 @@ import org.jboss.jandex.ParameterizedType; import org.jboss.jandex.PrimitiveType.Primitive; import org.jboss.jandex.Type; +import org.jboss.jandex.Type.Kind; import org.jboss.jandex.TypeVariable; +import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; import io.quarkus.panache.common.impl.GenerateBridge; @@ -57,6 +60,12 @@ public static String getDescriptor(MethodInfo method, Function t return descriptor.toString(); } + public static String getDescriptor(Type type, Function typeArgMapper) { + StringBuilder sb = new StringBuilder(); + toSignature(sb, type, typeArgMapper, true); + return sb.toString(); + } + static void toSignature(StringBuilder sb, Type type, Function typeArgMapper, boolean erased) { switch (type.kind()) { case ARRAY: @@ -163,6 +172,30 @@ public static int getReturnInstruction(String typeDescriptor) { } } + public static int getReturnInstruction(Type jandexType) { + if (jandexType.kind() == Kind.PRIMITIVE) { + switch (jandexType.asPrimitiveType().primitive()) { + case BOOLEAN: + case BYTE: + case SHORT: + case INT: + case CHAR: + return Opcodes.IRETURN; + case DOUBLE: + return Opcodes.DRETURN; + case FLOAT: + return Opcodes.FRETURN; + case LONG: + return Opcodes.LRETURN; + default: + throw new IllegalArgumentException("Unknown primitive type: " + jandexType); + } + } else if (jandexType.kind() == Kind.VOID) { + return Opcodes.RETURN; + } + return Opcodes.ARETURN; + } + /** * Checks if the {@link ClassInfo} contains a method * @@ -201,4 +234,203 @@ public static boolean containsMethod(ClassInfo classInfo, MethodInfo methodInfo) } return false; } + + public static void visitLdc(MethodVisitor mv, Type jandexType) { + switch (jandexType.kind()) { + case ARRAY: + mv.visitLdcInsn(org.objectweb.asm.Type.getType(jandexType.name().toString('/').replace('.', '/'))); + break; + case CLASS: + case PARAMETERIZED_TYPE: + mv.visitLdcInsn(org.objectweb.asm.Type.getType("L" + jandexType.name().toString('/') + ";")); + break; + case PRIMITIVE: + switch (jandexType.asPrimitiveType().primitive()) { + case BOOLEAN: + mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/Boolean", "TYPE", "Ljava/lang/Class;"); + break; + case BYTE: + mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/Byte", "TYPE", "Ljava/lang/Class;"); + break; + case CHAR: + mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/Character", "TYPE", "Ljava/lang/Class;"); + break; + case DOUBLE: + mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/Double", "TYPE", "Ljava/lang/Class;"); + break; + case FLOAT: + mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/Float", "TYPE", "Ljava/lang/Class;"); + break; + case INT: + mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/Integer", "TYPE", "Ljava/lang/Class;"); + break; + case LONG: + mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/Long", "TYPE", "Ljava/lang/Class;"); + break; + case SHORT: + mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/Short", "TYPE", "Ljava/lang/Class;"); + break; + default: + throw new IllegalArgumentException("Unknown primitive type: " + jandexType); + } + break; + case TYPE_VARIABLE: + List bounds = jandexType.asTypeVariable().bounds(); + if (bounds.isEmpty()) + mv.visitLdcInsn(org.objectweb.asm.Type.getType(Object.class)); + else + visitLdc(mv, bounds.get(0)); + break; + case UNRESOLVED_TYPE_VARIABLE: + mv.visitLdcInsn(org.objectweb.asm.Type.getType(Object.class)); + break; + case VOID: + mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/Void", "TYPE", "Ljava/lang/Class;"); + break; + case WILDCARD_TYPE: + visitLdc(mv, jandexType.asWildcardType().extendsBound()); + break; + default: + throw new IllegalArgumentException("Unknown jandex type: " + jandexType); + } + } + + public static void boxIfRequired(MethodVisitor mv, Type jandexType) { + if (jandexType.kind() == Kind.PRIMITIVE) { + switch (jandexType.asPrimitiveType().primitive()) { + case BOOLEAN: + mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/Boolean", "valueOf", "(Z)Ljava/lang/Boolean;", false); + break; + case BYTE: + mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/Byte", "valueOf", "(B)Ljava/lang/Byte;", false); + break; + case CHAR: + mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/Character", "valueOf", "(C)Ljava/lang/Character;", + false); + break; + case DOUBLE: + mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/Double", "valueOf", "(D)Ljava/lang/Double;", false); + break; + case FLOAT: + mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/Float", "valueOf", "(F)Ljava/lang/Float;", false); + break; + case INT: + mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;", false); + break; + case LONG: + mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/Long", "valueOf", "(J)Ljava/lang/Long;", false); + break; + case SHORT: + mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/Short", "valueOf", "(S)Ljava/lang/Short;", false); + break; + default: + throw new IllegalArgumentException("Unknown primitive type: " + jandexType); + } + } + } + + public static int getLoadOpcode(Type jandexType) { + if (jandexType.kind() == Kind.PRIMITIVE) { + switch (jandexType.asPrimitiveType().primitive()) { + case BOOLEAN: + case BYTE: + case SHORT: + case INT: + case CHAR: + return Opcodes.ILOAD; + case DOUBLE: + return Opcodes.DLOAD; + case FLOAT: + return Opcodes.FLOAD; + case LONG: + return Opcodes.LLOAD; + default: + throw new IllegalArgumentException("Unknown primitive type: " + jandexType); + } + } + return Opcodes.ALOAD; + } + + public static void unboxIfRequired(MethodVisitor mv, Type jandexType) { + if (jandexType.kind() == Kind.PRIMITIVE) { + switch (jandexType.asPrimitiveType().primitive()) { + case BOOLEAN: + unbox(mv, "java/lang/Boolean", "booleanValue", "Z"); + break; + case BYTE: + unbox(mv, "java/lang/Byte", "byteValue", "B"); + break; + case CHAR: + unbox(mv, "java/lang/Character", "charValue", "C"); + break; + case DOUBLE: + unbox(mv, "java/lang/Double", "doubleValue", "D"); + break; + case FLOAT: + unbox(mv, "java/lang/Float", "floatValue", "F"); + break; + case INT: + unbox(mv, "java/lang/Integer", "intValue", "I"); + break; + case LONG: + unbox(mv, "java/lang/Long", "longValue", "J"); + break; + case SHORT: + unbox(mv, "java/lang/Short", "shortValue", "S"); + break; + default: + throw new IllegalArgumentException("Unknown primitive type: " + jandexType); + } + } + } + + private static void unbox(MethodVisitor mv, String owner, String methodName, String returnTypeSignature) { + mv.visitTypeInsn(Opcodes.CHECKCAST, owner); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, owner, methodName, "()" + returnTypeSignature, false); + } + + public static Type[] getParameterTypes(String methodDescriptor) { + String argsSignature = methodDescriptor.substring(methodDescriptor.indexOf('(') + 1, methodDescriptor.lastIndexOf(')')); + List args = new ArrayList<>(); + char[] chars = argsSignature.toCharArray(); + int dimensions = 0; + int start = 0; + for (int i = 0; i < chars.length; i++) { + char c = chars[i]; + switch (c) { + case 'Z': + case 'B': + case 'C': + case 'D': + case 'F': + case 'I': + case 'J': + case 'S': + args.add(Type.create(DotName.createSimple(argsSignature.substring(start, i + 1)), dimensions > 0 ? Kind.ARRAY : Kind.PRIMITIVE)); + dimensions = 0; + start = i + 1; + break; + case 'L': + int end = argsSignature.indexOf(';', i); + String binaryName = argsSignature.substring(i + 1, end); + // arrays take the entire signature + if (dimensions > 0) { + args.add(Type.create(DotName.createSimple(argsSignature.substring(start, end + 1)), Kind.ARRAY)); + dimensions = 0; + } else { + // class names take only the binary name + args.add(Type.create(DotName.createSimple(binaryName.replace('/', '.')), Kind.CLASS)); + } + i = end; // we will have a ++ to get after the ; + start = i + 1; + break; + case '[': + dimensions++; + break; + default: + throw new IllegalStateException("Invalid signature char: " + c); + } + } + return args.toArray(new Type[0]); + } } diff --git a/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheEntityEnhancer.java b/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheEntityEnhancer.java index 947706411ef4d4..b5b0a8452a077b 100644 --- a/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheEntityEnhancer.java +++ b/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheEntityEnhancer.java @@ -1,5 +1,7 @@ package io.quarkus.panache.common.deployment; +import java.lang.reflect.Modifier; +import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -45,10 +47,13 @@ public abstract class PanacheEntityEnhancer methodCustomizers; - public PanacheEntityEnhancer(IndexView index, DotName panacheEntityBaseName) { + public PanacheEntityEnhancer(IndexView index, DotName panacheEntityBaseName, + List methodCustomizers) { this.panacheEntityBaseClassInfo = index.getClassByName(panacheEntityBaseName); this.indexView = index; + this.methodCustomizers = methodCustomizers; } @Override @@ -63,11 +68,13 @@ protected abstract static class PanacheEntityClassVisitor modelInfo; private ClassInfo panacheEntityBaseClassInfo; protected ClassInfo entityInfo; + protected List methodCustomizers; public PanacheEntityClassVisitor(String className, ClassVisitor outputClassVisitor, MetamodelInfo> modelInfo, ClassInfo panacheEntityBaseClassInfo, - ClassInfo entityInfo) { + ClassInfo entityInfo, + List methodCustomizers) { super(Gizmo.ASM_API_VERSION, outputClassVisitor); thisClass = Type.getType("L" + className.replace('.', '/') + ";"); this.modelInfo = modelInfo; @@ -75,6 +82,7 @@ public PanacheEntityClassVisitor(String className, ClassVisitor outputClassVisit fields = entityModel != null ? entityModel.fields : null; this.panacheEntityBaseClassInfo = panacheEntityBaseClassInfo; this.entityInfo = entityInfo; + this.methodCustomizers = methodCustomizers; } @Override @@ -124,6 +132,17 @@ public MethodVisitor visitMethod(int access, String methodName, String descripto methods.add(methodName + "/" + descriptor); } MethodVisitor superVisitor = super.visitMethod(access, methodName, descriptor, signature, exceptions); + if(Modifier.isStatic(access) + && Modifier.isPublic(access) + && (access & Opcodes.ACC_SYNTHETIC) == 0) { + org.jboss.jandex.Type[] argTypes = JandexUtil.getParameterTypes(descriptor); + MethodInfo method = this.entityInfo.method(methodName, argTypes); + if(method == null) { + throw new IllegalStateException("Could not find indexed method: "+thisClass+"."+methodName+" with descriptor "+descriptor + +" and arg types "+Arrays.toString(argTypes)); + } + superVisitor = new PanacheMethodCustomizerVisitor(superVisitor, method, thisClass, methodCustomizers); + } return new PanacheFieldAccessMethodVisitor(superVisitor, thisClass.getInternalName(), methodName, descriptor, modelInfo); } @@ -165,6 +184,9 @@ private void generateMethod(MethodInfo method, AnnotationValue targetReturnTypeE mv.visitParameter(method.parameterName(i), 0 /* modifiers */); } mv.visitCode(); + for (PanacheMethodCustomizer customizer : methodCustomizers) { + customizer.customize(thisClass, method, mv); + } // inject model injectModel(mv); for (int i = 0; i < parameters.size(); i++) { diff --git a/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheMethodCustomizer.java b/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheMethodCustomizer.java new file mode 100644 index 00000000000000..bf9a86b488bf8e --- /dev/null +++ b/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheMethodCustomizer.java @@ -0,0 +1,11 @@ +package io.quarkus.panache.common.deployment; + +import org.jboss.jandex.MethodInfo; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Type; + +public interface PanacheMethodCustomizer { + + public void customize(Type entityClassSignature, MethodInfo method, MethodVisitor mv); + +} diff --git a/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheMethodCustomizerBuildItem.java b/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheMethodCustomizerBuildItem.java new file mode 100644 index 00000000000000..293d6cf78669ab --- /dev/null +++ b/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheMethodCustomizerBuildItem.java @@ -0,0 +1,18 @@ +package io.quarkus.panache.common.deployment; + +import io.quarkus.builder.item.MultiBuildItem; + +/** + * Build item to declare that a {@link PanacheMethodCustomizer} should be used on Panache-enhanced methods. + */ +public final class PanacheMethodCustomizerBuildItem extends MultiBuildItem { + private PanacheMethodCustomizer methodCustomizer; + + public PanacheMethodCustomizerBuildItem(PanacheMethodCustomizer methodCustomizer) { + this.methodCustomizer = methodCustomizer; + } + + public PanacheMethodCustomizer getMethodCustomizer() { + return methodCustomizer; + } +} diff --git a/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheMethodCustomizerVisitor.java b/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheMethodCustomizerVisitor.java new file mode 100644 index 00000000000000..21403b660db00f --- /dev/null +++ b/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheMethodCustomizerVisitor.java @@ -0,0 +1,31 @@ +package io.quarkus.panache.common.deployment; + +import java.util.List; + +import org.jboss.jandex.MethodInfo; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Type; + +import io.quarkus.gizmo.Gizmo; + +public class PanacheMethodCustomizerVisitor extends MethodVisitor { + + private List methodCustomizers; + private MethodInfo method; + private Type thisClass; + + public PanacheMethodCustomizerVisitor(MethodVisitor superVisitor, MethodInfo method, Type thisClass, List methodCustomizers) { + super(Gizmo.ASM_API_VERSION, superVisitor); + this.thisClass = thisClass; + this.method = method; + this.methodCustomizers = methodCustomizers; + } + + @Override + public void visitCode() { + super.visitCode(); + for (PanacheMethodCustomizer customizer : methodCustomizers) { + customizer.customize(thisClass, method, mv); + } + } +} diff --git a/extensions/panache/panache-mock/deployment/pom.xml b/extensions/panache/panache-mock/deployment/pom.xml new file mode 100644 index 00000000000000..444e0a11672e67 --- /dev/null +++ b/extensions/panache/panache-mock/deployment/pom.xml @@ -0,0 +1,59 @@ + + + + quarkus-panache-mock-parent + io.quarkus + 999-SNAPSHOT + ../ + + 4.0.0 + + quarkus-panache-mock-deployment + Quarkus - Panache - Mock - Deployment + + + + io.quarkus + quarkus-core-deployment + + + io.quarkus + quarkus-panache-common-deployment + + + io.quarkus + quarkus-panache-mock + + + io.quarkus + quarkus-arc + + + org.jboss + jandex + + + org.ow2.asm + asm + + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + + diff --git a/extensions/panache/panache-mock/deployment/src/main/java/io/quarkus/panache/mock/deployment/PanacheMockMethodCustomizer.java b/extensions/panache/panache-mock/deployment/src/main/java/io/quarkus/panache/mock/deployment/PanacheMockMethodCustomizer.java new file mode 100644 index 00000000000000..f874cb6ece11f5 --- /dev/null +++ b/extensions/panache/panache-mock/deployment/src/main/java/io/quarkus/panache/mock/deployment/PanacheMockMethodCustomizer.java @@ -0,0 +1,123 @@ +package io.quarkus.panache.mock.deployment; + +import org.jboss.jandex.MethodInfo; +import org.jboss.jandex.Type.Kind; +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; + +import io.quarkus.panache.common.deployment.JandexUtil; +import io.quarkus.panache.common.deployment.PanacheMethodCustomizer; +import io.quarkus.panache.mock.PanacheMock; + +public class PanacheMockMethodCustomizer implements PanacheMethodCustomizer { + + private final static String PANACHE_MOCK_BINARY_NAME = PanacheMock.class.getName().replace('.', '/'); + private final static String PANACHE_MOCK_INVOKE_REAL_METHOD_EXCEPTION_BINARY_NAME = PanacheMock.InvokeRealMethodException.class + .getName().replace('.', '/'); + + @Override + public void customize(Type entityClassSignature, MethodInfo method, MethodVisitor mv) { + /* + * Generated code: + * + * if(PanacheMock.IsMockEnabled && PanacheMock.isMocked(TestClass.class)) { + * try { + * return (int)PanacheMock.mockMethod(TestClass.class, "foo", new Class[] {int.class}, new Object[] {arg}); + * } catch (PanacheMock.InvokeRealMethodException e) { + * // fall-through + * } + * } + * + * Bytecode approx: + * + * 0: getstatic #16 // Field PanacheMock.IsMockEnabled:Z + * 3: ifeq 50 + * 6: ldc #1 // class MyTestMockito$TestClass + * 8: invokestatic #22 // Method PanacheMock.isMocked:(Ljava/lang/Class;)Z + * 11: ifeq 50 + * 14: ldc #1 // class MyTestMockito$TestClass + * 16: ldc #26 // String foo + * + * 18: iconst_1 + * 19: anewarray #27 // class java/lang/Class + * 22: dup + * 23: iconst_0 + * 24: getstatic #29 // Field java/lang/Integer.TYPE:Ljava/lang/Class; + * 27: aastore + * + * 28: iconst_1 + * 29: anewarray #3 // class java/lang/Object + * 32: dup + * 33: iconst_0 + * 34: iload_0 + * 35: invokestatic #35 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; + * 38: aastore + * + * 39: invokestatic #39 // Method + * PanacheMock.mockMethod:(Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/Class;[Ljava/lang/Object;)Ljava/lang/Object; + * 42: checkcast #30 // class java/lang/Integer + * 45: invokevirtual #43 // Method java/lang/Integer.intValue:()I + * 48: ireturn + * 49: astore_1 + */ + Label realMethodLabel = new Label(); + + mv.visitFieldInsn(Opcodes.GETSTATIC, PANACHE_MOCK_BINARY_NAME, "IsMockEnabled", "Z"); + mv.visitJumpInsn(Opcodes.IFEQ, realMethodLabel); + + mv.visitLdcInsn(entityClassSignature); + mv.visitMethodInsn(Opcodes.INVOKESTATIC, PANACHE_MOCK_BINARY_NAME, "isMocked", "(Ljava/lang/Class;)Z", false); + mv.visitJumpInsn(Opcodes.IFEQ, realMethodLabel); + + Label tryStart = new Label(); + Label tryEnd = new Label(); + Label tryHandler = new Label(); + mv.visitTryCatchBlock(tryStart, tryEnd, tryHandler, PANACHE_MOCK_INVOKE_REAL_METHOD_EXCEPTION_BINARY_NAME); + mv.visitLabel(tryStart); + + mv.visitLdcInsn(entityClassSignature); + mv.visitLdcInsn(method.name()); + + mv.visitLdcInsn(method.parameters().size()); + mv.visitTypeInsn(Opcodes.ANEWARRAY, "java/lang/Class"); + + int i = 0; + for (org.jboss.jandex.Type paramType : method.parameters()) { + mv.visitInsn(Opcodes.DUP); + mv.visitLdcInsn(i); + JandexUtil.visitLdc(mv, paramType); + mv.visitInsn(Opcodes.AASTORE); + i++; + } + + mv.visitLdcInsn(method.parameters().size()); + mv.visitTypeInsn(Opcodes.ANEWARRAY, "java/lang/Object"); + + i = 0; + for (org.jboss.jandex.Type paramType : method.parameters()) { + mv.visitInsn(Opcodes.DUP); + mv.visitLdcInsn(i); + mv.visitVarInsn(JandexUtil.getLoadOpcode(paramType), i); + JandexUtil.boxIfRequired(mv, paramType); + mv.visitInsn(Opcodes.AASTORE); + i++; + } + + mv.visitMethodInsn(Opcodes.INVOKESTATIC, PANACHE_MOCK_BINARY_NAME, "mockMethod", + "(Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/Class;[Ljava/lang/Object;)Ljava/lang/Object;", false); + JandexUtil.unboxIfRequired(mv, method.returnType()); + if (method.returnType().kind() != Kind.PRIMITIVE) { + mv.visitTypeInsn(Opcodes.CHECKCAST, method.returnType().name().toString('/')); + } + + mv.visitInsn(JandexUtil.getReturnInstruction(method.returnType())); + + mv.visitLabel(tryHandler); + mv.visitInsn(Opcodes.POP); + mv.visitLabel(tryEnd); + + mv.visitLabel(realMethodLabel); + } +} diff --git a/extensions/panache/panache-mock/deployment/src/main/java/io/quarkus/panache/mock/deployment/PanacheMockResourceProcessor.java b/extensions/panache/panache-mock/deployment/src/main/java/io/quarkus/panache/mock/deployment/PanacheMockResourceProcessor.java new file mode 100644 index 00000000000000..f70a104aef936d --- /dev/null +++ b/extensions/panache/panache-mock/deployment/src/main/java/io/quarkus/panache/mock/deployment/PanacheMockResourceProcessor.java @@ -0,0 +1,17 @@ +package io.quarkus.panache.mock.deployment; + +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.panache.common.deployment.PanacheMethodCustomizerBuildItem; + +public final class PanacheMockResourceProcessor { + @BuildStep + FeatureBuildItem featureBuildItem() { + return new FeatureBuildItem(FeatureBuildItem.PANACHE_MOCK); + } + + @BuildStep + public PanacheMethodCustomizerBuildItem addMocks() { + return new PanacheMethodCustomizerBuildItem(new PanacheMockMethodCustomizer()); + } +} diff --git a/extensions/panache/panache-mock/pom.xml b/extensions/panache/panache-mock/pom.xml new file mode 100644 index 00000000000000..87c2f129315915 --- /dev/null +++ b/extensions/panache/panache-mock/pom.xml @@ -0,0 +1,22 @@ + + + + quarkus-build-parent + io.quarkus + 999-SNAPSHOT + ../../../build-parent/pom.xml + + 4.0.0 + + quarkus-panache-mock-parent + Quarkus - Panache - Mock + pom + + runtime + deployment + + + + diff --git a/extensions/panache/panache-mock/runtime/pom.xml b/extensions/panache/panache-mock/runtime/pom.xml new file mode 100644 index 00000000000000..4afb50a51a9df7 --- /dev/null +++ b/extensions/panache/panache-mock/runtime/pom.xml @@ -0,0 +1,38 @@ + + + + quarkus-panache-mock-parent + io.quarkus + 999-SNAPSHOT + ../ + + 4.0.0 + + quarkus-panache-mock + Quarkus - Panache - Mock - Runtime + An opinionated approach to make Hibernate as easy as possible + + + io.quarkus + quarkus-core + + + io.quarkus + quarkus-arc + + + org.mockito + mockito-core + + + + + + + io.quarkus + quarkus-bootstrap-maven-plugin + + + + diff --git a/extensions/panache/panache-mock/runtime/src/main/java/io/quarkus/panache/mock/PanacheMock.java b/extensions/panache/panache-mock/runtime/src/main/java/io/quarkus/panache/mock/PanacheMock.java new file mode 100644 index 00000000000000..3ae49b5348c7ab --- /dev/null +++ b/extensions/panache/panache-mock/runtime/src/main/java/io/quarkus/panache/mock/PanacheMock.java @@ -0,0 +1,81 @@ +package io.quarkus.panache.mock; + +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + +import org.mockito.Mockito; +import org.mockito.internal.invocation.DefaultInvocationFactory; +import org.mockito.internal.invocation.InterceptedInvocation; +import org.mockito.internal.invocation.RealMethod; +import org.mockito.internal.util.MockUtil; +import org.mockito.invocation.MockHandler; +import org.mockito.mock.MockCreationSettings; + +public class PanacheMock { + + public static boolean IsMockEnabled = false; + + private final static Map, Object> mocks = new HashMap<>(); + + @SuppressWarnings("unchecked") + public static T getMock(Class klass) { + return (T) mocks.get(klass); + } + + public static void mock(Class... classes) { + for (Class klass : classes) { + mocks.computeIfAbsent(klass, v -> Mockito.mock(klass)); + } + IsMockEnabled = !mocks.isEmpty(); + } + + public static void reset() { + mocks.clear(); + IsMockEnabled = false; + } + + public static boolean isMocked(Class klass) { + return mocks.containsKey(klass); + } + + public static Object mockMethod(Class klass, String methodName, Class[] parameterTypes, Object[] args) + throws InvokeRealMethodException { + try { + Method invokedMethod = klass.getDeclaredMethod(methodName, parameterTypes); + Object mock = getMock(klass); + MockCreationSettings settings = MockUtil.getMockSettings(mock); + MyRealMethod myRealMethod = new MyRealMethod(); + InterceptedInvocation invocation = DefaultInvocationFactory.createInvocation(mock, invokedMethod, args, + myRealMethod, settings, null); + MockHandler handler = MockUtil.getMockHandler(mock); + return handler.handle(invocation); + } catch (InvokeRealMethodException e) { + throw e; + } catch (RuntimeException e) { + throw e; + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + @SuppressWarnings("serial") + public static class InvokeRealMethodException extends Exception { + } + + @SuppressWarnings("serial") + public static class MyRealMethod implements RealMethod { + + @Override + public boolean isInvokable() { + return true; + } + + @Override + public Object invoke() throws Throwable { + throw new InvokeRealMethodException(); + } + + } + +} diff --git a/extensions/panache/panache-mock/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/panache/panache-mock/runtime/src/main/resources/META-INF/quarkus-extension.yaml new file mode 100644 index 00000000000000..50f6f3d5edd651 --- /dev/null +++ b/extensions/panache/panache-mock/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -0,0 +1,15 @@ +--- +name: "Panache Mocking" +metadata: + keywords: + - "hibernate-orm-panache" + - "mongodb-panache" + - "panache" + - "hibernate" + - "jpa" + - "test" + - "mock" + guide: "https://quarkus.io/guides/hibernate-orm-panache" + categories: + - "data" + status: "stable" diff --git a/extensions/panache/pom.xml b/extensions/panache/pom.xml index 34220535ae0420..8a36507c66f7fe 100644 --- a/extensions/panache/pom.xml +++ b/extensions/panache/pom.xml @@ -15,6 +15,7 @@ pom panache-common + panache-mock hibernate-orm-panache mongodb-panache panacheql diff --git a/integration-tests/hibernate-orm-panache/pom.xml b/integration-tests/hibernate-orm-panache/pom.xml index 9006bfdc8c3684..183f3f7c413adc 100644 --- a/integration-tests/hibernate-orm-panache/pom.xml +++ b/integration-tests/hibernate-orm-panache/pom.xml @@ -78,6 +78,19 @@ quarkus-jackson test + + io.quarkus + quarkus-panache-mock + + + + net.bytebuddy + byte-buddy + + + com.fasterxml.jackson.module jackson-module-jaxb-annotations diff --git a/integration-tests/hibernate-orm-panache/src/test/java/io/quarkus/it/panache/PanacheFunctionalityTest.java b/integration-tests/hibernate-orm-panache/src/test/java/io/quarkus/it/panache/PanacheFunctionalityTest.java index add79fb5bd40ff..86fb15980aa20f 100644 --- a/integration-tests/hibernate-orm-panache/src/test/java/io/quarkus/it/panache/PanacheFunctionalityTest.java +++ b/integration-tests/hibernate-orm-panache/src/test/java/io/quarkus/it/panache/PanacheFunctionalityTest.java @@ -4,19 +4,24 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import java.io.StringWriter; +import java.util.Collections; import javax.json.bind.Jsonb; import javax.json.bind.JsonbBuilder; +import javax.ws.rs.WebApplicationException; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import io.quarkus.panache.mock.PanacheMock; import io.quarkus.test.junit.DisabledOnNativeImage; import io.quarkus.test.junit.QuarkusTest; import io.restassured.RestAssured; @@ -163,4 +168,42 @@ public void testBug7721() { public void testBug8254() { RestAssured.when().get("/test/8254").then().body(is("OK")); } + + @AfterEach + public void afterEach() { + PanacheMock.reset(); + } + + @DisabledOnNativeImage + @Test + public void testPanacheMocking() { + PanacheMock.mock(Person.class); + + Assertions.assertEquals(0, Person.count()); + + Mockito.when(Person.count()).thenReturn(23l); + Assertions.assertEquals(23, Person.count()); + + Mockito.when(Person.count()).thenReturn(42l); + Assertions.assertEquals(42, Person.count()); + + Mockito.when(Person.count()).thenCallRealMethod(); + Assertions.assertEquals(0, Person.count()); + + Mockito.verify(PanacheMock.getMock(Person.class), Mockito.times(4)).count(); + + Mockito.when(Person.findById(12l)).thenReturn(p); + Assertions.assertSame(p, Person.findById(12l)); + Assertions.assertNull(Person.findById(42l)); + + Mockito.when(Person.findById(12l)).thenThrow(new WebApplicationException()); + try { + Person.findById(12l); + Assertions.fail(); + } catch (WebApplicationException x) { + } + + Mockito.when(Person.findOrdered()).thenReturn(Collections.emptyList()); + Assertions.assertTrue(Person.findOrdered().isEmpty()); + } }