From 5cd37f67c2e3fa5e1094a6d0c22194bfff1dc084 Mon Sep 17 00:00:00 2001 From: Stephane Epardaud Date: Wed, 8 Apr 2020 16:45:44 +0200 Subject: [PATCH 1/2] Panache ORM/Mongo: fixed bridge generation Now it's all automated, no more forgetting methods to bridge --- .../PanacheJpaRepositoryEnhancer.java | 54 ---- .../PanacheMongoRepositoryEnhancer.java | 32 -- .../panache/common/deployment/JandexUtil.java | 303 ++++++++++++++++-- .../deployment/PanacheEntityEnhancer.java | 17 +- .../deployment/PanacheRepositoryEnhancer.java | 134 ++++++-- .../it/panache/PanacheFunctionalityTest.java | 35 ++ .../panache/MongodbPanacheResourceTest.java | 28 ++ 7 files changed, 456 insertions(+), 147 deletions(-) diff --git a/extensions/panache/hibernate-orm-panache/deployment/src/main/java/io/quarkus/hibernate/orm/panache/deployment/PanacheJpaRepositoryEnhancer.java b/extensions/panache/hibernate-orm-panache/deployment/src/main/java/io/quarkus/hibernate/orm/panache/deployment/PanacheJpaRepositoryEnhancer.java index 920d0f76dc527..0153edbc59f87 100644 --- a/extensions/panache/hibernate-orm-panache/deployment/src/main/java/io/quarkus/hibernate/orm/panache/deployment/PanacheJpaRepositoryEnhancer.java +++ b/extensions/panache/hibernate-orm-panache/deployment/src/main/java/io/quarkus/hibernate/orm/panache/deployment/PanacheJpaRepositoryEnhancer.java @@ -5,11 +5,9 @@ import org.jboss.jandex.IndexView; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.MethodVisitor; -import org.objectweb.asm.Opcodes; import io.quarkus.hibernate.orm.panache.PanacheRepository; import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase; -import io.quarkus.panache.common.deployment.JandexUtil; import io.quarkus.panache.common.deployment.PanacheRepositoryEnhancer; public class PanacheJpaRepositoryEnhancer extends PanacheRepositoryEnhancer { @@ -50,58 +48,6 @@ protected String getPanacheOperationsBinaryName() { return PanacheJpaEntityEnhancer.JPA_OPERATIONS_BINARY_NAME; } - @Override - public void visitEnd() { - // Bridge for findById, but only if we actually know the end entity (which we don't for intermediate - // abstract repositories that haven't fixed their entity type yet - if (!"Ljava/lang/Object;".equals(entitySignature)) { - if (!JandexUtil.containsMethod(daoClassInfo, "findById", - Object.class.getName(), - Object.class.getName())) { - MethodVisitor mv = super.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_SYNTHETIC | Opcodes.ACC_BRIDGE, - "findById", - "(Ljava/lang/Object;)Ljava/lang/Object;", - null, - null); - mv.visitParameter("id", 0); - mv.visitCode(); - mv.visitIntInsn(Opcodes.ALOAD, 0); - mv.visitIntInsn(Opcodes.ALOAD, 1); - - mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, - daoBinaryName, - "findById", - "(Ljava/lang/Object;)" + entitySignature, false); - mv.visitInsn(Opcodes.ARETURN); - mv.visitMaxs(0, 0); - mv.visitEnd(); - } - if (!JandexUtil.containsMethod(daoClassInfo, "findById", - Object.class.getName(), - Object.class.getName(), "javax.persistence.LockModeType")) { - MethodVisitor mv = super.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_SYNTHETIC | Opcodes.ACC_BRIDGE, - "findById", - "(Ljava/lang/Object;Ljavax/persistence/LockModeType;)Ljava/lang/Object;", - null, - null); - mv.visitParameter("id", 0); - mv.visitParameter("lockModeType", 0); - mv.visitCode(); - mv.visitIntInsn(Opcodes.ALOAD, 0); - mv.visitIntInsn(Opcodes.ALOAD, 1); - mv.visitIntInsn(Opcodes.ALOAD, 2); - mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, - daoBinaryName, - "findById", - "(Ljava/lang/Object;Ljavax/persistence/LockModeType;)" + entitySignature, false); - mv.visitInsn(Opcodes.ARETURN); - mv.visitMaxs(0, 0); - mv.visitEnd(); - } - } - super.visitEnd(); - } - @Override protected void injectModel(MethodVisitor mv) { // inject Class diff --git a/extensions/panache/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/PanacheMongoRepositoryEnhancer.java b/extensions/panache/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/PanacheMongoRepositoryEnhancer.java index 17b245e852b2f..29ab719398877 100644 --- a/extensions/panache/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/PanacheMongoRepositoryEnhancer.java +++ b/extensions/panache/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/PanacheMongoRepositoryEnhancer.java @@ -5,11 +5,9 @@ import org.jboss.jandex.IndexView; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.MethodVisitor; -import org.objectweb.asm.Opcodes; import io.quarkus.mongodb.panache.PanacheMongoRepository; import io.quarkus.mongodb.panache.PanacheMongoRepositoryBase; -import io.quarkus.panache.common.deployment.JandexUtil; import io.quarkus.panache.common.deployment.PanacheRepositoryEnhancer; public class PanacheMongoRepositoryEnhancer extends PanacheRepositoryEnhancer { @@ -49,36 +47,6 @@ protected String getPanacheOperationsBinaryName() { return PanacheMongoEntityEnhancer.MONGO_OPERATIONS_BINARY_NAME; } - @Override - public void visitEnd() { - // Bridge for findById, but only if we actually know the end entity (which we don't for intermediate - // abstract repositories that haven't fixed their entity type yet - if (!entitySignature.equals("Ljava/lang/Object;")) { - if (!JandexUtil.containsMethod(daoClassInfo, "findById", - Object.class.getName(), - Object.class.getName())) { - MethodVisitor mv = super.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_SYNTHETIC | Opcodes.ACC_BRIDGE, - "findById", - "(Ljava/lang/Object;)Ljava/lang/Object;", - null, - null); - mv.visitParameter("id", 0); - mv.visitCode(); - mv.visitIntInsn(Opcodes.ALOAD, 0); - mv.visitIntInsn(Opcodes.ALOAD, 1); - mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, - daoBinaryName, - "findById", - "(Ljava/lang/Object;)" + entitySignature, false); - mv.visitInsn(Opcodes.ARETURN); - mv.visitMaxs(0, 0); - mv.visitEnd(); - } - } - - super.visitEnd(); - } - @Override protected void injectModel(MethodVisitor mv) { // inject Class 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 5a1359ed69f36..42bab758fae6d 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 @@ -1,19 +1,18 @@ package io.quarkus.panache.common.deployment; -import static java.util.stream.Collectors.toList; - -import java.util.Arrays; +import java.util.ArrayList; import java.util.List; import java.util.function.Function; import org.jboss.jandex.ArrayType; -import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; import org.jboss.jandex.MethodInfo; 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 +56,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,42 +168,276 @@ public static int getReturnInstruction(String typeDescriptor) { } } - /** - * Checks if the {@link ClassInfo} contains a method - * - * @param classInfo the {@link ClassInfo} instance - * @param methodName the method name to check - * @param parameters the parameter types, if any - * @return true if the {@link ClassInfo} parameter contains this method - */ - public static boolean containsMethod(ClassInfo classInfo, - String methodName, - String returnType, - String... parameters) { - List types = Arrays.stream(parameters).map(JandexUtil::toClassType).collect(toList()); - for (MethodInfo methodInfo : classInfo.methods()) { - if (methodInfo.name().equals(methodName) && methodInfo.parameters().equals(types)) { - return true; + 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; + } + + 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); } } - return false; } - static Type toClassType(String type) { - return Type.create(DotName.createSimple(type), Type.Kind.CLASS); + 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 boolean containsMethod(ClassInfo classInfo, MethodInfo methodInfo) { - if (classInfo.methods().contains(methodInfo)) { - return true; + 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': + args.add(Type.create(DotName.createSimple("boolean"), + dimensions > 0 ? Kind.ARRAY : Kind.PRIMITIVE)); + dimensions = 0; + start = i + 1; + break; + case 'B': + args.add(Type.create(DotName.createSimple("byte"), + dimensions > 0 ? Kind.ARRAY : Kind.PRIMITIVE)); + dimensions = 0; + start = i + 1; + break; + case 'C': + args.add(Type.create(DotName.createSimple("char"), + dimensions > 0 ? Kind.ARRAY : Kind.PRIMITIVE)); + dimensions = 0; + start = i + 1; + break; + case 'D': + args.add(Type.create(DotName.createSimple("double"), + dimensions > 0 ? Kind.ARRAY : Kind.PRIMITIVE)); + dimensions = 0; + start = i + 1; + break; + case 'F': + args.add(Type.create(DotName.createSimple("float"), + dimensions > 0 ? Kind.ARRAY : Kind.PRIMITIVE)); + dimensions = 0; + start = i + 1; + break; + case 'I': + args.add(Type.create(DotName.createSimple("int"), + dimensions > 0 ? Kind.ARRAY : Kind.PRIMITIVE)); + dimensions = 0; + start = i + 1; + break; + case 'J': + args.add(Type.create(DotName.createSimple("long"), + dimensions > 0 ? Kind.ARRAY : Kind.PRIMITIVE)); + dimensions = 0; + start = i + 1; + break; + case 'S': + args.add(Type.create(DotName.createSimple("short"), + 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); + } } - // MethodInfo may not belong to the same declaring class. Check signature - for (MethodInfo classMethodInfo : classInfo.methods()) { - if (classMethodInfo.name().equals(methodInfo.name()) && - classMethodInfo.parameters().equals(methodInfo.parameters())) { - return true; + return args.toArray(new Type[0]); + } +<<<<<<< HEAD +======= + + public static int getParameterSize(Type paramType) { + if (paramType.kind() == Kind.PRIMITIVE) { + switch (paramType.asPrimitiveType().primitive()) { + case DOUBLE: + case LONG: + return 2; } } - return false; + return 1; } +>>>>>>> d0599c8c73... fixup enhancers } 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 947706411ef4d..7558c726a474b 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 @@ -58,8 +58,8 @@ protected abstract static class PanacheEntityClassVisitor fields; - // set of name + "/" + descriptor (only for suspected accessor names) - private Set methods = new HashSet<>(); + // set of name + "/" + descriptor + private Set userMethods = new HashSet<>(); private MetamodelInfo modelInfo; private ClassInfo panacheEntityBaseClassInfo; protected ClassInfo entityInfo; @@ -118,11 +118,7 @@ public void visitEnd() { @Override public MethodVisitor visitMethod(int access, String methodName, String descriptor, String signature, String[] exceptions) { - if (methodName.startsWith("get") - || methodName.startsWith("set") - || methodName.startsWith("is")) { - methods.add(methodName + "/" + descriptor); - } + userMethods.add(methodName + "/" + descriptor); MethodVisitor superVisitor = super.visitMethod(access, methodName, descriptor, signature, exceptions); return new PanacheFieldAccessMethodVisitor(superVisitor, thisClass.getInternalName(), methodName, descriptor, modelInfo); @@ -134,7 +130,8 @@ public void visitEnd() { for (MethodInfo method : panacheEntityBaseClassInfo.methods()) { // Do not generate a method that already exists - if (!JandexUtil.containsMethod(entityInfo, method)) { + String descriptor = JandexUtil.getDescriptor(method, name -> null); + if (!userMethods.contains(method.name() + "/" + descriptor)) { AnnotationInstance bridge = method.annotation(JandexUtil.DOTNAME_GENERATE_BRIDGE); if (bridge != null) { generateMethod(method, bridge.value("targetReturnTypeErased")); @@ -202,7 +199,7 @@ private void generateAccessors() { // Getter String getterName = field.getGetterName(); String getterDescriptor = "()" + field.descriptor; - if (!methods.contains(getterName + "/" + getterDescriptor)) { + if (!userMethods.contains(getterName + "/" + getterDescriptor)) { MethodVisitor mv = super.visitMethod(Opcodes.ACC_PUBLIC, getterName, getterDescriptor, field.signature == null ? null : "()" + field.signature, null); mv.visitCode(); @@ -226,7 +223,7 @@ private void generateAccessors() { // Setter String setterName = field.getSetterName(); String setterDescriptor = "(" + field.descriptor + ")V"; - if (!methods.contains(setterName + "/" + setterDescriptor)) { + if (!userMethods.contains(setterName + "/" + setterDescriptor)) { MethodVisitor mv = super.visitMethod(Opcodes.ACC_PUBLIC, setterName, setterDescriptor, field.signature == null ? null : "(" + field.signature + ")V", null); mv.visitCode(); diff --git a/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheRepositoryEnhancer.java b/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheRepositoryEnhancer.java index 9cdb35fc1cfc1..b5a20bc5e8adc 100644 --- a/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheRepositoryEnhancer.java +++ b/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheRepositoryEnhancer.java @@ -1,7 +1,11 @@ package io.quarkus.panache.common.deployment; import java.lang.reflect.Modifier; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.function.BiFunction; import org.jboss.jandex.AnnotationInstance; @@ -37,10 +41,15 @@ protected static abstract class PanacheRepositoryClassVisitor extends ClassVisit protected Type entityType; protected String entitySignature; protected String entityBinaryType; + protected String idSignature; + protected String idBinaryType; protected String daoBinaryName; protected ClassInfo daoClassInfo; protected ClassInfo panacheRepositoryBaseClassInfo; protected IndexView indexView; + protected Map typeArguments = new HashMap<>(); + // set of name + "/" + descriptor + protected Set userMethods = new HashSet<>(); public PanacheRepositoryClassVisitor(String className, ClassVisitor outputClassVisitor, ClassInfo panacheRepositoryBaseClassInfo, IndexView indexView) { @@ -67,30 +76,37 @@ public void visit(int version, int access, String name, String signature, String final String repositoryClassName = name.replace('/', '.'); - String foundEntityType = findEntityBinaryTypeForPanacheRepository(repositoryClassName, - getPanacheRepositoryDotName()); + String[] foundTypeArguments = findEntityTypeArgumentsForPanacheRepository(repositoryClassName, + getPanacheRepositoryBaseDotName()); - if (foundEntityType == null) { - foundEntityType = findEntityBinaryTypeForPanacheRepository(repositoryClassName, - getPanacheRepositoryBaseDotName()); - } - - entityBinaryType = foundEntityType; + entityBinaryType = foundTypeArguments[0]; entitySignature = "L" + entityBinaryType + ";"; entityType = Type.getType(entitySignature); + idBinaryType = foundTypeArguments[1]; + idSignature = "L" + idBinaryType + ";"; + + typeArguments.put("Entity", entitySignature); + typeArguments.put("Id", idSignature); + } + + @Override + public MethodVisitor visitMethod(int access, String methodName, String descriptor, String signature, + String[] exceptions) { + userMethods.add(methodName + "/" + descriptor); + return super.visitMethod(access, methodName, descriptor, signature, exceptions); } - private String findEntityBinaryTypeForPanacheRepository(String repositoryClassName, DotName repositoryDotName) { + private String[] findEntityTypeArgumentsForPanacheRepository(String repositoryClassName, DotName repositoryDotName) { for (ClassInfo classInfo : indexView.getAllKnownImplementors(repositoryDotName)) { if (repositoryClassName.equals(classInfo.name().toString())) { - return recursivelyFindEntityTypeFromClass(classInfo.name(), repositoryDotName); + return recursivelyFindEntityTypeArgumentsFromClass(classInfo.name(), repositoryDotName); } } return null; } - private String recursivelyFindEntityTypeFromClass(DotName clazz, DotName repositoryDotName) { + private String[] recursivelyFindEntityTypeArgumentsFromClass(DotName clazz, DotName repositoryDotName) { if (clazz.equals(OBJECT_DOT_NAME)) { return null; } @@ -101,26 +117,105 @@ private String recursivelyFindEntityTypeFromClass(DotName clazz, DotName reposit throw new IllegalStateException( "Failed to find supertype " + repositoryDotName + " from entity class " + clazz); org.jboss.jandex.Type entityType = typeParameters.get(0); - return entityType.name().toString().replace('.', '/'); + org.jboss.jandex.Type idType = typeParameters.get(1); + return new String[] { + entityType.name().toString().replace('.', '/'), + idType.name().toString().replace('.', '/') + }; } @Override public void visitEnd() { for (MethodInfo method : panacheRepositoryBaseClassInfo.methods()) { // Do not generate a method that already exists - if (!JandexUtil.containsMethod(daoClassInfo, method)) { + String descriptor = JandexUtil.getDescriptor(method, name -> typeArguments.get(name)); + if (!userMethods.contains(method.name() + "/" + descriptor)) { AnnotationInstance bridge = method.annotation(JandexUtil.DOTNAME_GENERATE_BRIDGE); if (bridge != null) { - generateMethod(method, bridge.value("targetReturnTypeErased")); + generateModelBridge(method, bridge.value("targetReturnTypeErased")); + if (needsJvmBridge(method)) { + generateJvmBridge(method); + } } } } super.visitEnd(); } - private void generateMethod(MethodInfo method, AnnotationValue targetReturnTypeErased) { - String descriptor = JandexUtil.getDescriptor(method, name -> name.equals("Entity") ? entitySignature : null); - String signature = JandexUtil.getSignature(method, name -> name.equals("Entity") ? entitySignature : null); + private boolean needsJvmBridge(MethodInfo method) { + if (needsJvmBridge(method.returnType())) + return true; + for (org.jboss.jandex.Type paramType : method.parameters()) { + if (needsJvmBridge(paramType)) + return true; + } + return false; + } + + private boolean needsJvmBridge(org.jboss.jandex.Type type) { + if (type.kind() == Kind.TYPE_VARIABLE) { + String typeParamName = type.asTypeVariable().identifier(); + return typeArguments.containsKey(typeParamName); + } + return false; + } + + private void generateJvmBridge(MethodInfo method) { + // get a bounds-erased descriptor + String descriptor = JandexUtil.getDescriptor(method, name -> null); + // make sure we need a bridge + if (!userMethods.contains(method.name() + "/" + descriptor)) { + MethodVisitor mv = super.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_SYNTHETIC | Opcodes.ACC_BRIDGE, + method.name(), + descriptor, + null, + null); + List parameters = method.parameters(); + for (int i = 0; i < parameters.size(); i++) { + mv.visitParameter(method.parameterName(i), 0 /* modifiers */); + } + mv.visitCode(); + // this + mv.visitIntInsn(Opcodes.ALOAD, 0); + // each param + for (int i = 0; i < parameters.size(); i++) { + org.jboss.jandex.Type paramType = parameters.get(i); + if (paramType.kind() == Kind.PRIMITIVE) + throw new IllegalStateException("BUG: Don't know how to generate JVM bridge method for " + method + + ": has primitive parameters"); + mv.visitIntInsn(Opcodes.ALOAD, i + 1); + if (paramType.kind() == Kind.TYPE_VARIABLE) { + String typeParamName = paramType.asTypeVariable().identifier(); + switch (typeParamName) { + case "Entity": + mv.visitTypeInsn(Opcodes.CHECKCAST, entityBinaryType); + break; + case "Id": + mv.visitTypeInsn(Opcodes.CHECKCAST, idBinaryType); + break; + } + } + } + + String targetDescriptor = JandexUtil.getDescriptor(method, name -> typeArguments.get(name)); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, + daoBinaryName, + method.name(), + targetDescriptor, false); + String targetReturnTypeDescriptor = targetDescriptor.substring(targetDescriptor.indexOf(')') + 1); + mv.visitInsn(JandexUtil.getReturnInstruction(targetReturnTypeDescriptor)); + mv.visitMaxs(0, 0); + mv.visitEnd(); + } + + } + + private void generateModelBridge(MethodInfo method, AnnotationValue targetReturnTypeErased) { + String descriptor = JandexUtil.getDescriptor(method, name -> typeArguments.get(name)); + // JpaOperations erases the Id type to Object + String descriptorForJpaOperations = JandexUtil.getDescriptor(method, + name -> name.equals("Entity") ? entitySignature : null); + String signature = JandexUtil.getSignature(method, name -> typeArguments.get(name)); List parameters = method.parameters(); String castTo = null; @@ -134,7 +229,8 @@ private void generateMethod(MethodInfo method, AnnotationValue targetReturnTypeE castTo = type.name().toString('/'); } - MethodVisitor mv = super.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_SYNTHETIC, + // Note: we can't use SYNTHETIC here because otherwise Mockito will never mock these methods + MethodVisitor mv = super.visitMethod(Opcodes.ACC_PUBLIC, method.name(), descriptor, signature, @@ -148,7 +244,7 @@ private void generateMethod(MethodInfo method, AnnotationValue targetReturnTypeE mv.visitIntInsn(Opcodes.ALOAD, i + 1); } // inject Class - String forwardingDescriptor = "(" + getModelDescriptor() + descriptor.substring(1); + String forwardingDescriptor = "(" + getModelDescriptor() + descriptorForJpaOperations.substring(1); if (castTo != null) { // return type is erased to Object int lastParen = forwardingDescriptor.lastIndexOf(')'); 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 add79fb5bd40f..112ece19a4bee 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,9 +4,12 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import java.io.StringWriter; +import java.util.Optional; +import javax.inject.Inject; import javax.json.bind.Jsonb; import javax.json.bind.JsonbBuilder; +import javax.persistence.LockModeType; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; @@ -17,6 +20,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase; import io.quarkus.test.junit.DisabledOnNativeImage; import io.quarkus.test.junit.QuarkusTest; import io.restassured.RestAssured; @@ -163,4 +167,35 @@ public void testBug7721() { public void testBug8254() { RestAssured.when().get("/test/8254").then().body(is("OK")); } + + @Inject + PersonRepository realPersonRepository; + + @DisabledOnNativeImage + @Test + public void testPanacheRepositoryBridges() { + // normal method call + Assertions.assertNull(realPersonRepository.findById(0l)); + // bridge call + Assertions.assertNull(((PanacheRepositoryBase) realPersonRepository).findById(0l)); + // normal method call + Assertions.assertNull(realPersonRepository.findById(0l, LockModeType.NONE)); + // bridge call + Assertions.assertNull(((PanacheRepositoryBase) realPersonRepository).findById(0l, LockModeType.NONE)); + + // normal method call + Assertions.assertEquals(Optional.empty(), realPersonRepository.findByIdOptional(0l)); + // bridge call + Assertions.assertEquals(Optional.empty(), ((PanacheRepositoryBase) realPersonRepository).findByIdOptional(0l)); + // normal method call + Assertions.assertEquals(Optional.empty(), realPersonRepository.findByIdOptional(0l, LockModeType.NONE)); + // bridge call + Assertions.assertEquals(Optional.empty(), + ((PanacheRepositoryBase) realPersonRepository).findByIdOptional(0l, LockModeType.NONE)); + + // normal method call + Assertions.assertEquals(false, realPersonRepository.deleteById(0l)); + // bridge call + Assertions.assertEquals(false, ((PanacheRepositoryBase) realPersonRepository).deleteById(0l)); + } } diff --git a/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/MongodbPanacheResourceTest.java b/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/MongodbPanacheResourceTest.java index dedf714d3983f..a111e25f2704a 100644 --- a/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/MongodbPanacheResourceTest.java +++ b/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/MongodbPanacheResourceTest.java @@ -10,6 +10,9 @@ import java.util.Date; import java.util.GregorianCalendar; import java.util.List; +import java.util.Optional; + +import javax.inject.Inject; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -21,7 +24,10 @@ import io.quarkus.it.mongodb.panache.book.BookDetail; import io.quarkus.it.mongodb.panache.person.Person; +import io.quarkus.it.mongodb.panache.person.PersonRepository; +import io.quarkus.mongodb.panache.PanacheMongoRepositoryBase; import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.DisabledOnNativeImage; import io.quarkus.test.junit.QuarkusTest; import io.restassured.RestAssured; import io.restassured.common.mapper.TypeRef; @@ -354,4 +360,26 @@ public void testMoreEntityFunctionalities() { public void testMoreRepositoryFunctionalities() { get("/test/imperative/repository").then().statusCode(200); } + + @Inject + PersonRepository realPersonRepository; + + @DisabledOnNativeImage + @Test + public void testPanacheRepositoryBridges() { + // normal method call + Assertions.assertNull(realPersonRepository.findById(0l)); + // bridge call + Assertions.assertNull(((PanacheMongoRepositoryBase) realPersonRepository).findById(0l)); + + // normal method call + Assertions.assertEquals(Optional.empty(), realPersonRepository.findByIdOptional(0l)); + // bridge call + Assertions.assertEquals(Optional.empty(), ((PanacheMongoRepositoryBase) realPersonRepository).findByIdOptional(0l)); + + // normal method call + Assertions.assertEquals(false, realPersonRepository.deleteById(0l)); + // bridge call + Assertions.assertEquals(false, ((PanacheMongoRepositoryBase) realPersonRepository).deleteById(0l)); + } } From 3a5680ea367c5dad60655dcef0ca9765f14d5efe Mon Sep 17 00:00:00 2001 From: Stephane Epardaud Date: Wed, 8 Apr 2020 16:46:33 +0200 Subject: [PATCH 2/2] Added panche-mock module and docs --- bom/runtime/pom.xml | 5 + .../asciidoc/getting-started-testing.adoc | 4 + .../main/asciidoc/hibernate-orm-panache.adoc | 180 +++++++++++++++++- docs/src/main/asciidoc/mongodb-panache.adoc | 175 +++++++++++++++++ .../deployment/PanacheJpaEntityEnhancer.java | 13 +- .../deployment/PanacheResourceProcessor.java | 11 +- .../hibernate-orm-panache/runtime/pom.xml | 10 + .../PanacheMongoEntityEnhancer.java | 12 +- .../deployment/PanacheResourceProcessor.java | 20 +- .../ReactivePanacheMongoEntityEnhancer.java | 12 +- .../panache/common/deployment/JandexUtil.java | 3 - .../deployment/PanacheEntityEnhancer.java | 28 ++- .../deployment/PanacheMethodCustomizer.java | 11 ++ .../PanacheMethodCustomizerBuildItem.java | 18 ++ .../PanacheMethodCustomizerVisitor.java | 32 ++++ extensions/panache/panache-mock/pom.xml | 37 ++++ .../io/quarkus/panache/mock/PanacheMock.java | 110 +++++++++++ .../mock/impl/PanacheMockAfterEachTest.java | 13 ++ .../impl/PanacheMockBuildChainCustomizer.java | 37 ++++ .../impl/PanacheMockMethodCustomizer.java | 125 ++++++++++++ ...uildchain.TestBuildChainCustomizerProducer | 1 + ...unit.callback.QuarkusTestAfterEachCallback | 1 + extensions/panache/pom.xml | 1 + .../hibernate-orm-panache/pom.xml | 11 ++ .../it/panache/MockablePersonRepository.java | 14 ++ .../java/io/quarkus/it/panache/Person.java | 6 +- .../quarkus/it/panache/PersonRepository.java | 5 + .../io/quarkus/it/panache/TestEndpoint.java | 4 + .../it/panache/PanacheFunctionalityTest.java | 35 ---- .../it/panache/PanacheMockingTest.java | 136 +++++++++++++ integration-tests/mongodb-panache/pom.xml | 5 + .../person/MockablePersonRepository.java | 15 ++ .../mongodb/panache/person/PersonEntity.java | 7 + .../person/PersonRepositoryResource.java | 4 + .../panache/MongodbPanacheMockingTest.java | 131 +++++++++++++ .../panache/MongodbPanacheResourceTest.java | 28 --- 36 files changed, 1169 insertions(+), 91 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/pom.xml create mode 100644 extensions/panache/panache-mock/src/main/java/io/quarkus/panache/mock/PanacheMock.java create mode 100644 extensions/panache/panache-mock/src/main/java/io/quarkus/panache/mock/impl/PanacheMockAfterEachTest.java create mode 100644 extensions/panache/panache-mock/src/main/java/io/quarkus/panache/mock/impl/PanacheMockBuildChainCustomizer.java create mode 100644 extensions/panache/panache-mock/src/main/java/io/quarkus/panache/mock/impl/PanacheMockMethodCustomizer.java create mode 100644 extensions/panache/panache-mock/src/main/resources/META-INF/services/io.quarkus.test.junit.buildchain.TestBuildChainCustomizerProducer create mode 100644 extensions/panache/panache-mock/src/main/resources/META-INF/services/io.quarkus.test.junit.callback.QuarkusTestAfterEachCallback create mode 100644 integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/MockablePersonRepository.java create mode 100644 integration-tests/hibernate-orm-panache/src/test/java/io/quarkus/it/panache/PanacheMockingTest.java create mode 100644 integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/person/MockablePersonRepository.java create mode 100644 integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/MongodbPanacheMockingTest.java diff --git a/bom/runtime/pom.xml b/bom/runtime/pom.xml index 96505db6bfed4..0c7f7f0f495a3 100644 --- a/bom/runtime/pom.xml +++ b/bom/runtime/pom.xml @@ -427,6 +427,11 @@ quarkus-panache-common ${project.version} + + io.quarkus + quarkus-panache-mock + ${project.version} + io.quarkus quarkus-panacheql diff --git a/docs/src/main/asciidoc/getting-started-testing.adoc b/docs/src/main/asciidoc/getting-started-testing.adoc index 3f400611aaeae..3289fba901168 100644 --- a/docs/src/main/asciidoc/getting-started-testing.adoc +++ b/docs/src/main/asciidoc/getting-started-testing.adoc @@ -569,6 +569,10 @@ public class GreetingResourceTest { ---- <1> Indicate that this injection point is meant to use an instance of `RestClient`. +=== Mocking with Panache + +If you are using the `quarkus-hibernate-orm-panache` or `quarkus-mongodb-panache` extensions, check out the link:hibernate-orm-panache#mocking[Hibernate ORM with Panache Mocking] and link:mongodb-panache#mocking[MongoDB with Panache Mocking] documentation for the easiest way to mock your data access. + == Test Bootstrap Configuration Options There are a few system properties that can be used to tune the bootstrap of the test, specifically its classpath. diff --git a/docs/src/main/asciidoc/hibernate-orm-panache.adoc b/docs/src/main/asciidoc/hibernate-orm-panache.adoc index 8dcd69ff252ef..92d6492a2fab5 100644 --- a/docs/src/main/asciidoc/hibernate-orm-panache.adoc +++ b/docs/src/main/asciidoc/hibernate-orm-panache.adoc @@ -611,7 +611,7 @@ public void create(Parameter parameter){ Panache provides direct support for database locking with your entity/repository, using `findById(Object, LockModeType)` or `find().withLock(LockModeType)`. -The following examples are for the entity pattern, but the same can be used with repositories. +The following examples are for the active record pattern, but the same can be used with repositories. === First: Locking using findById(). @@ -686,6 +686,184 @@ public class PersonRepository implements PanacheRepositoryBase { } ---- +== Mocking + +=== Using the active record pattern + +If you are using the active record pattern you cannot use Mockito directly as it does not support mocking static methods, +but you can use the `quarkus-panache-mock` module which allows you to use Mockito to mock all provided static +methods, including your own. + +Add this dependency to your `pom.xml`: + +[source,xml] +---- + + io.quarkus + quarkus-panache-mock + test + +---- + + +Given this simple entity: + +[source,java] +---- +@Entity +public class Person extends PanacheEntity { + + public String name; + + public static List findOrdered() { + return find("ORDER BY name").list(); + } +} +---- + +You can write your mocking test like this: + +[source,java] +---- +@QuarkusTest +public class PanacheFunctionalityTest { + + @Test + public void testPanacheMocking() { + PanacheMock.mock(Person.class); + + // Mocked classes always return a default value + Assertions.assertEquals(0, Person.count()); + + // Now let's specify the return value + Mockito.when(Person.count()).thenReturn(23l); + Assertions.assertEquals(23, Person.count()); + + // Now let's change the return value + Mockito.when(Person.count()).thenReturn(42l); + Assertions.assertEquals(42, Person.count()); + + // Now let's call the original method + Mockito.when(Person.count()).thenCallRealMethod(); + Assertions.assertEquals(0, Person.count()); + + // Check that we called it 4 times + PanacheMock.verify(Person.class, Mockito.times(4)).count();// <1> + + // Mock only with specific parameters + Person p = new Person(); + Mockito.when(Person.findById(12l)).thenReturn(p); + Assertions.assertSame(p, Person.findById(12l)); + Assertions.assertNull(Person.findById(42l)); + + // Mock throwing + Mockito.when(Person.findById(12l)).thenThrow(new WebApplicationException()); + Assertions.assertThrows(WebApplicationException.class, () -> Person.findById(12l)); + + // We can even mock your custom methods + Mockito.when(Person.findOrdered()).thenReturn(Collections.emptyList()); + Assertions.assertTrue(Person.findOrdered().isEmpty()); + + PanacheMock.verify(Person.class).findOrdered(); + PanacheMock.verify(Person.class, Mockito.atLeastOnce()).findById(Mockito.any()); + PanacheMock.verifyNoMoreInteractions(Person.class); + } +} +---- +<1> Be sure to call your `verify` methods on `PanacheMock` rather than `Mockito`, otherwise you won't know +what mock object to pass. + +=== Using the repository pattern + +If you are using the repository pattern you can use Mockito directly, using the `quarkus-junit5-mockito` module, +which makes mocking beans much easier: + +[source,java] +---- + + io.quarkus + quarkus-junit5-mockito + test + +---- + +Given this simple entity: + +[source,java] +---- +@Entity +public class Person { + + @Id + @GeneratedValue + public Long id; + + public String name; +} +---- + +And this repository: + +[source,java] +---- +@ApplicationScoped +public class PersonRepository implements PanacheRepository { + public List findOrdered() { + return find("ORDER BY name").list(); + } +} +---- + +You can write your mocking test like this: + +[source,java] +---- +@QuarkusTest +public class PanacheFunctionalityTest { + @InjectMock + PersonRepository personRepository; + + @Test + public void testPanacheRepositoryMocking() throws Throwable { + // Mocked classes always return a default value + Assertions.assertEquals(0, personRepository.count()); + + // Now let's specify the return value + Mockito.when(personRepository.count()).thenReturn(23l); + Assertions.assertEquals(23, personRepository.count()); + + // Now let's change the return value + Mockito.when(personRepository.count()).thenReturn(42l); + Assertions.assertEquals(42, personRepository.count()); + + // Now let's call the original method + Mockito.when(personRepository.count()).thenCallRealMethod(); + Assertions.assertEquals(0, personRepository.count()); + + // Check that we called it 4 times + Mockito.verify(personRepository, Mockito.times(4)).count(); + + // Mock only with specific parameters + Person p = new Person(); + Mockito.when(personRepository.findById(12l)).thenReturn(p); + Assertions.assertSame(p, personRepository.findById(12l)); + Assertions.assertNull(personRepository.findById(42l)); + + // Mock throwing + Mockito.when(personRepository.findById(12l)).thenThrow(new WebApplicationException()); + Assertions.assertThrows(WebApplicationException.class, () -> personRepository.findById(12l)); + + Mockito.when(personRepository.findOrdered()).thenReturn(Collections.emptyList()); + Assertions.assertTrue(personRepository.findOrdered().isEmpty()); + + // We can even mock your custom methods + Mockito.verify(personRepository).findOrdered(); + Mockito.verify(personRepository, Mockito.atLeastOnce()).findById(Mockito.any()); + Mockito.verifyNoMoreInteractions(personRepository); + } +} +---- + == How and why we simplify Hibernate ORM mappings When it comes to writing Hibernate ORM entities, there are a number of annoying things that users have grown used to diff --git a/docs/src/main/asciidoc/mongodb-panache.adoc b/docs/src/main/asciidoc/mongodb-panache.adoc index 474dc668c89bd..3b3f9d7add49c 100644 --- a/docs/src/main/asciidoc/mongodb-panache.adoc +++ b/docs/src/main/asciidoc/mongodb-panache.adoc @@ -867,6 +867,181 @@ public Multi streamPersons() { TIP: `@SseElementType(MediaType.APPLICATION_JSON)` tells RESTEasy to serialize the object in JSON. +== Mocking + +=== Using the active-record pattern + +If you are using the active-record pattern you cannot use Mockito directly as it does not support mocking static methods, +but you can use the `quarkus-panache-mock` module which allows you to use Mockito to mock all provided static +methods, including your own. + +Add this dependency to your `pom.xml`: + +[source,xml] +---- + + io.quarkus + quarkus-panache-mock + test + +---- + +Given this simple entity: + +[source,java] +---- +public class Person extends PanacheMongoEntity { + + public String name; + + public static List findOrdered() { + return findAll(Sort.by("lastname", "firstname")).list(); + } +} +---- + +You can write your mocking test like this: + +[source,java] +---- +@QuarkusTest +public class PanacheFunctionalityTest { + + @Test + public void testPanacheMocking() { + PanacheMock.mock(Person.class); + + // Mocked classes always return a default value + Assertions.assertEquals(0, Person.count()); + + // Now let's specify the return value + Mockito.when(Person.count()).thenReturn(23l); + Assertions.assertEquals(23, Person.count()); + + // Now let's change the return value + Mockito.when(Person.count()).thenReturn(42l); + Assertions.assertEquals(42, Person.count()); + + // Now let's call the original method + Mockito.when(Person.count()).thenCallRealMethod(); + Assertions.assertEquals(0, Person.count()); + + // Check that we called it 4 times + PanacheMock.verify(Person.class, Mockito.times(4)).count();// <1> + + // Mock only with specific parameters + Person p = new Person(); + Mockito.when(Person.findById(12l)).thenReturn(p); + Assertions.assertSame(p, Person.findById(12l)); + Assertions.assertNull(Person.findById(42l)); + + // Mock throwing + Mockito.when(Person.findById(12l)).thenThrow(new WebApplicationException()); + Assertions.assertThrows(WebApplicationException.class, () -> Person.findById(12l)); + + // We can even mock your custom methods + Mockito.when(Person.findOrdered()).thenReturn(Collections.emptyList()); + Assertions.assertTrue(Person.findOrdered().isEmpty()); + + PanacheMock.verify(Person.class).findOrdered(); + PanacheMock.verify(Person.class, Mockito.atLeastOnce()).findById(Mockito.any()); + PanacheMock.verifyNoMoreInteractions(Person.class); + } +} +---- +<1> Be sure to call your `verify` methods on `PanacheMock` rather than `Mockito`, otherwise you won't know +what mock object to pass. + +=== Using the repository pattern + +If you are using the repository pattern you can use Mockito directly, using the `quarkus-junit5-mockito` module, +which makes mocking beans much easier: + +[source,java] +---- + + io.quarkus + quarkus-junit5-mockito + test + +---- + +Given this simple entity: + +[source,java] +---- +public class Person { + + @BsonId + public Long id; + + public String name; +} +---- + +And this repository: + +[source,java] +---- +@ApplicationScoped +public class PersonRepository implements PanacheMongoRepository { + public List findOrdered() { + return findAll(Sort.by("lastname", "firstname")).list(); + } +} +---- + +You can write your mocking test like this: + +[source,java] +---- +@QuarkusTest +public class PanacheFunctionalityTest { + @InjectMock + PersonRepository personRepository; + + @Test + public void testPanacheRepositoryMocking() throws Throwable { + // Mocked classes always return a default value + Assertions.assertEquals(0, personRepository.count()); + + // Now let's specify the return value + Mockito.when(personRepository.count()).thenReturn(23l); + Assertions.assertEquals(23, personRepository.count()); + + // Now let's change the return value + Mockito.when(personRepository.count()).thenReturn(42l); + Assertions.assertEquals(42, personRepository.count()); + + // Now let's call the original method + Mockito.when(personRepository.count()).thenCallRealMethod(); + Assertions.assertEquals(0, personRepository.count()); + + // Check that we called it 4 times + Mockito.verify(personRepository, Mockito.times(4)).count(); + + // Mock only with specific parameters + Person p = new Person(); + Mockito.when(personRepository.findById(12l)).thenReturn(p); + Assertions.assertSame(p, personRepository.findById(12l)); + Assertions.assertNull(personRepository.findById(42l)); + + // Mock throwing + Mockito.when(personRepository.findById(12l)).thenThrow(new WebApplicationException()); + Assertions.assertThrows(WebApplicationException.class, () -> personRepository.findById(12l)); + + Mockito.when(personRepository.findOrdered()).thenReturn(Collections.emptyList()); + Assertions.assertTrue(personRepository.findOrdered().isEmpty()); + + // We can even mock your custom methods + Mockito.verify(personRepository).findOrdered(); + Mockito.verify(personRepository, Mockito.atLeastOnce()).findById(Mockito.any()); + Mockito.verifyNoMoreInteractions(personRepository); + } +} +---- + + == How and why we simplify MongoDB API When it comes to writing MongoDB entities, there are a number of annoying things that users have grown used to 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 f29c14ad090c2..7e3135519826c 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 7de5c41758178..3d64066db93f5 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 @@ -6,6 +6,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import javax.persistence.EntityManager; import javax.persistence.NamedQueries; @@ -38,6 +39,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 { @@ -76,7 +79,11 @@ void build(CombinedIndexBuildItem index, BuildProducer transformers, HibernateEnhancersRegisteredBuildItem hibernateMarker, BuildProducer entityClasses, - BuildProducer namedQueries) throws Exception { + BuildProducer namedQueries, + 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<>(); @@ -107,7 +114,7 @@ void build(CombinedIndexBuildItem index, namedQueries.produce(new NamedQueryEntityClassBuildStep(parameterType.name().toString(), typeNamedQueries)); } - 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 619864a3a1296..db189f7b4f32d 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/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/PanacheMongoEntityEnhancer.java b/extensions/panache/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/PanacheMongoEntityEnhancer.java index e786ff47d73e9..45b64eb2f4ee7 100644 --- a/extensions/panache/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/PanacheMongoEntityEnhancer.java +++ b/extensions/panache/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/PanacheMongoEntityEnhancer.java @@ -2,6 +2,7 @@ import java.lang.reflect.Modifier; import java.util.HashMap; +import java.util.List; import java.util.Map; import org.bson.codecs.pojo.annotations.BsonIgnore; @@ -19,6 +20,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 PanacheMongoEntityEnhancer extends PanacheEntityEnhancer>> { public final static String MONGO_OPERATIONS_NAME = MongoOperations.class.getName(); @@ -28,23 +30,23 @@ public class PanacheMongoEntityEnhancer extends PanacheEntityEnhancer entities = new HashMap<>(); - public PanacheMongoEntityEnhancer(IndexView index) { - super(index, PanacheResourceProcessor.DOTNAME_PANACHE_ENTITY_BASE); + public PanacheMongoEntityEnhancer(IndexView index, List methodCustomizers) { + super(index, PanacheResourceProcessor.DOTNAME_PANACHE_ENTITY_BASE, methodCustomizers); modelInfo = new MetamodelInfo<>(); } @Override public ClassVisitor apply(String className, ClassVisitor outputClassVisitor) { return new PanacheMongoEntityClassVisitor(className, outputClassVisitor, modelInfo, panacheEntityBaseClassInfo, - indexView.getClassByName(DotName.createSimple(className))); + indexView.getClassByName(DotName.createSimple(className)), methodCustomizers); } static class PanacheMongoEntityClassVisitor extends PanacheEntityClassVisitor { public PanacheMongoEntityClassVisitor(String className, ClassVisitor outputClassVisitor, MetamodelInfo> 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/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/PanacheResourceProcessor.java b/extensions/panache/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/PanacheResourceProcessor.java index 96ae8cdf948f6..25bfd54ddcb43 100644 --- a/extensions/panache/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/PanacheResourceProcessor.java +++ b/extensions/panache/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/PanacheResourceProcessor.java @@ -7,6 +7,7 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; import org.bson.codecs.pojo.annotations.BsonProperty; import org.bson.types.ObjectId; @@ -51,6 +52,8 @@ import io.quarkus.mongodb.panache.reactive.ReactivePanacheMongoRepositoryBase; 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 class PanacheResourceProcessor { @@ -133,7 +136,11 @@ void buildImperative(CombinedIndexBuildItem index, BuildProducer transformers, BuildProducer reflectiveClass, BuildProducer propertyMappingClass, - BuildProducer entityClasses) { + BuildProducer entityClasses, + List methodCustomizersBuildItems) { + + List methodCustomizers = methodCustomizersBuildItems.stream() + .map(bi -> bi.getMethodCustomizer()).collect(Collectors.toList()); PanacheMongoRepositoryEnhancer daoEnhancer = new PanacheMongoRepositoryEnhancer(index.getIndex()); Set daoClasses = new HashSet<>(); @@ -167,7 +174,7 @@ void buildImperative(CombinedIndexBuildItem index, propertyMappingClass.produce(new PropertyMappingClassBuildStep(parameterType.name().toString())); } - PanacheMongoEntityEnhancer modelEnhancer = new PanacheMongoEntityEnhancer(index.getIndex()); + PanacheMongoEntityEnhancer modelEnhancer = new PanacheMongoEntityEnhancer(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 PanacheMongoEntity if we ask for subtypes of PanacheMongoEntityBase @@ -214,7 +221,11 @@ void buildMutiny(CombinedIndexBuildItem index, ApplicationIndexBuildItem applicationIndex, BuildProducer reflectiveClass, BuildProducer propertyMappingClass, - BuildProducer transformers) { + BuildProducer transformers, + List methodCustomizersBuildItems) { + + List methodCustomizers = methodCustomizersBuildItems.stream() + .map(bi -> bi.getMethodCustomizer()).collect(Collectors.toList()); ReactivePanacheMongoRepositoryEnhancer daoEnhancer = new ReactivePanacheMongoRepositoryEnhancer(index.getIndex()); Set daoClasses = new HashSet<>(); @@ -249,7 +260,8 @@ void buildMutiny(CombinedIndexBuildItem index, propertyMappingClass.produce(new PropertyMappingClassBuildStep(parameterType.name().toString())); } - ReactivePanacheMongoEntityEnhancer modelEnhancer = new ReactivePanacheMongoEntityEnhancer(index.getIndex()); + ReactivePanacheMongoEntityEnhancer modelEnhancer = new ReactivePanacheMongoEntityEnhancer(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 PanacheMongoEntity if we ask for subtypes of PanacheMongoEntityBase diff --git a/extensions/panache/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/ReactivePanacheMongoEntityEnhancer.java b/extensions/panache/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/ReactivePanacheMongoEntityEnhancer.java index f204029cdd645..679d8cb9794ce 100644 --- a/extensions/panache/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/ReactivePanacheMongoEntityEnhancer.java +++ b/extensions/panache/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/ReactivePanacheMongoEntityEnhancer.java @@ -2,6 +2,7 @@ import java.lang.reflect.Modifier; import java.util.HashMap; +import java.util.List; import java.util.Map; import org.bson.codecs.pojo.annotations.BsonIgnore; @@ -19,6 +20,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 ReactivePanacheMongoEntityEnhancer extends PanacheEntityEnhancer>> { public final static String MONGO_OPERATIONS_NAME = ReactiveMongoOperations.class.getName(); @@ -28,23 +30,23 @@ public class ReactivePanacheMongoEntityEnhancer extends PanacheEntityEnhancer entities = new HashMap<>(); - public ReactivePanacheMongoEntityEnhancer(IndexView index) { - super(index, PanacheResourceProcessor.DOTNAME_MUTINY_PANACHE_ENTITY_BASE); + public ReactivePanacheMongoEntityEnhancer(IndexView index, List methodCustomizers) { + super(index, PanacheResourceProcessor.DOTNAME_MUTINY_PANACHE_ENTITY_BASE, methodCustomizers); modelInfo = new MetamodelInfo<>(); } @Override public ClassVisitor apply(String className, ClassVisitor outputClassVisitor) { return new PanacheMongoEntityClassVisitor(className, outputClassVisitor, modelInfo, panacheEntityBaseClassInfo, - indexView.getClassByName(DotName.createSimple(className))); + indexView.getClassByName(DotName.createSimple(className)), methodCustomizers); } static class PanacheMongoEntityClassVisitor extends PanacheEntityClassVisitor { public PanacheMongoEntityClassVisitor(String className, ClassVisitor outputClassVisitor, MetamodelInfo> 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/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 42bab758fae6d..475e1990dc9ba 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 @@ -426,8 +426,6 @@ public static Type[] getParameterTypes(String methodDescriptor) { } return args.toArray(new Type[0]); } -<<<<<<< HEAD -======= public static int getParameterSize(Type paramType) { if (paramType.kind() == Kind.PRIMITIVE) { @@ -439,5 +437,4 @@ public static int getParameterSize(Type paramType) { } return 1; } ->>>>>>> d0599c8c73... fixup enhancers } 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 7558c726a474b..e8f5f1bf9a1f9 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 @@ -120,6 +128,19 @@ public MethodVisitor visitMethod(int access, String methodName, String descripto String[] exceptions) { userMethods.add(methodName + "/" + descriptor); MethodVisitor superVisitor = super.visitMethod(access, methodName, descriptor, signature, exceptions); + if (Modifier.isStatic(access) + && Modifier.isPublic(access) + && (access & Opcodes.ACC_SYNTHETIC) == 0 + && !methodCustomizers.isEmpty()) { + 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); } @@ -162,6 +183,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 0000000000000..bf9a86b488bf8 --- /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 0000000000000..293d6cf78669a --- /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 0000000000000..4fe968a6b3a53 --- /dev/null +++ b/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheMethodCustomizerVisitor.java @@ -0,0 +1,32 @@ +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/pom.xml b/extensions/panache/panache-mock/pom.xml new file mode 100644 index 0000000000000..df5f35d23e27d --- /dev/null +++ b/extensions/panache/panache-mock/pom.xml @@ -0,0 +1,37 @@ + + + + quarkus-build-parent + io.quarkus + 999-SNAPSHOT + ../../../build-parent/pom.xml + + 4.0.0 + + quarkus-panache-mock + Quarkus - Panache - Mock + Mocking with Panache + + + io.quarkus + quarkus-core + + + io.quarkus + quarkus-arc + + + io.quarkus + quarkus-panache-common-deployment + + + io.quarkus + quarkus-junit5-mockito + + + org.mockito + mockito-core + + + diff --git a/extensions/panache/panache-mock/src/main/java/io/quarkus/panache/mock/PanacheMock.java b/extensions/panache/panache-mock/src/main/java/io/quarkus/panache/mock/PanacheMock.java new file mode 100644 index 0000000000000..1c9748ef5ff27 --- /dev/null +++ b/extensions/panache/panache-mock/src/main/java/io/quarkus/panache/mock/PanacheMock.java @@ -0,0 +1,110 @@ +package io.quarkus.panache.mock; + +import java.lang.reflect.Method; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.mockito.Mockito; +import org.mockito.internal.debugging.LocationImpl; +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; +import org.mockito.verification.VerificationMode; + +public class PanacheMock { + + public static volatile boolean IsMockEnabled = false; + + private final static Map, Object> mocks = new ConcurrentHashMap<>(); + + @SuppressWarnings("unchecked") + public static T getMock(Class klass) { + return (T) mocks.get(klass); + } + + public static Object[] getMocks(Class... classes) { + Object[] mocks = new Object[classes.length]; + for (int i = 0; i < classes.length; i++) { + mocks[i] = getMock(classes[i]); + } + return mocks; + } + + 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, new LocationImpl(new Throwable(), true)); + 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); + } + } + + // + // Delegating + + public static T verify(Class klass) { + return Mockito.verify(getMock(klass)); + } + + public static T verify(Class klass, VerificationMode verificationMode) { + return Mockito.verify(getMock(klass), verificationMode); + } + + public static void verifyNoMoreInteractions(Class... classes) { + Mockito.verifyNoMoreInteractions(getMocks(classes)); + } + + public static void verifyNoInteractions(Class... classes) { + Mockito.verifyNoInteractions(getMocks(classes)); + } + + @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/src/main/java/io/quarkus/panache/mock/impl/PanacheMockAfterEachTest.java b/extensions/panache/panache-mock/src/main/java/io/quarkus/panache/mock/impl/PanacheMockAfterEachTest.java new file mode 100644 index 0000000000000..53a3b60cf959f --- /dev/null +++ b/extensions/panache/panache-mock/src/main/java/io/quarkus/panache/mock/impl/PanacheMockAfterEachTest.java @@ -0,0 +1,13 @@ +package io.quarkus.panache.mock.impl; + +import io.quarkus.panache.mock.PanacheMock; +import io.quarkus.test.junit.callback.QuarkusTestAfterEachCallback; + +public class PanacheMockAfterEachTest implements QuarkusTestAfterEachCallback { + + @Override + public void afterEach(Object testInstance) { + PanacheMock.reset(); + } + +} diff --git a/extensions/panache/panache-mock/src/main/java/io/quarkus/panache/mock/impl/PanacheMockBuildChainCustomizer.java b/extensions/panache/panache-mock/src/main/java/io/quarkus/panache/mock/impl/PanacheMockBuildChainCustomizer.java new file mode 100644 index 0000000000000..46334e1b6555b --- /dev/null +++ b/extensions/panache/panache-mock/src/main/java/io/quarkus/panache/mock/impl/PanacheMockBuildChainCustomizer.java @@ -0,0 +1,37 @@ +package io.quarkus.panache.mock.impl; + +import java.util.function.Consumer; + +import org.jboss.jandex.Index; + +import io.quarkus.builder.BuildChainBuilder; +import io.quarkus.builder.BuildContext; +import io.quarkus.builder.BuildStep; +import io.quarkus.deployment.builditem.LaunchModeBuildItem; +import io.quarkus.panache.common.deployment.PanacheMethodCustomizerBuildItem; +import io.quarkus.runtime.LaunchMode; +import io.quarkus.test.junit.buildchain.TestBuildChainCustomizerProducer; + +public final class PanacheMockBuildChainCustomizer implements TestBuildChainCustomizerProducer { + + @Override + public Consumer produce(Index testClassesIndex) { + return new Consumer() { + + @Override + public void accept(BuildChainBuilder buildChainBuilder) { + buildChainBuilder.addBuildStep(new BuildStep() { + @Override + public void execute(BuildContext context) { + LaunchModeBuildItem launchMode = context.consume(LaunchModeBuildItem.class); + if (launchMode.getLaunchMode() == LaunchMode.TEST) { + context.produce(new PanacheMethodCustomizerBuildItem(new PanacheMockMethodCustomizer())); + } + } + }).produces(PanacheMethodCustomizerBuildItem.class) + .consumes(LaunchModeBuildItem.class) + .build(); + } + }; + } +} diff --git a/extensions/panache/panache-mock/src/main/java/io/quarkus/panache/mock/impl/PanacheMockMethodCustomizer.java b/extensions/panache/panache-mock/src/main/java/io/quarkus/panache/mock/impl/PanacheMockMethodCustomizer.java new file mode 100644 index 0000000000000..00463e6a58df6 --- /dev/null +++ b/extensions/panache/panache-mock/src/main/java/io/quarkus/panache/mock/impl/PanacheMockMethodCustomizer.java @@ -0,0 +1,125 @@ +package io.quarkus.panache.mock.impl; + +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; + int paramSlot = 0; + for (org.jboss.jandex.Type paramType : method.parameters()) { + mv.visitInsn(Opcodes.DUP); + mv.visitLdcInsn(i); + mv.visitVarInsn(JandexUtil.getLoadOpcode(paramType), paramSlot); + JandexUtil.boxIfRequired(mv, paramType); + mv.visitInsn(Opcodes.AASTORE); + i++; + paramSlot += JandexUtil.getParameterSize(paramType); + } + + 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/src/main/resources/META-INF/services/io.quarkus.test.junit.buildchain.TestBuildChainCustomizerProducer b/extensions/panache/panache-mock/src/main/resources/META-INF/services/io.quarkus.test.junit.buildchain.TestBuildChainCustomizerProducer new file mode 100644 index 0000000000000..8e2b635300aac --- /dev/null +++ b/extensions/panache/panache-mock/src/main/resources/META-INF/services/io.quarkus.test.junit.buildchain.TestBuildChainCustomizerProducer @@ -0,0 +1 @@ +io.quarkus.panache.mock.impl.PanacheMockBuildChainCustomizer diff --git a/extensions/panache/panache-mock/src/main/resources/META-INF/services/io.quarkus.test.junit.callback.QuarkusTestAfterEachCallback b/extensions/panache/panache-mock/src/main/resources/META-INF/services/io.quarkus.test.junit.callback.QuarkusTestAfterEachCallback new file mode 100644 index 0000000000000..fc04ccca8c2d8 --- /dev/null +++ b/extensions/panache/panache-mock/src/main/resources/META-INF/services/io.quarkus.test.junit.callback.QuarkusTestAfterEachCallback @@ -0,0 +1 @@ +io.quarkus.panache.mock.impl.PanacheMockAfterEachTest diff --git a/extensions/panache/pom.xml b/extensions/panache/pom.xml index 34220535ae042..8a36507c66f7f 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 9006bfdc8c368..df67b02643d5c 100644 --- a/integration-tests/hibernate-orm-panache/pom.xml +++ b/integration-tests/hibernate-orm-panache/pom.xml @@ -78,6 +78,17 @@ quarkus-jackson test + + io.quarkus + quarkus-panache-mock + test + + + net.bytebuddy + byte-buddy + + + com.fasterxml.jackson.module jackson-module-jaxb-annotations diff --git a/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/MockablePersonRepository.java b/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/MockablePersonRepository.java new file mode 100644 index 0000000000000..a83ca6dcecaa1 --- /dev/null +++ b/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/MockablePersonRepository.java @@ -0,0 +1,14 @@ +package io.quarkus.it.panache; + +import java.util.List; + +import javax.enterprise.context.ApplicationScoped; + +import io.quarkus.hibernate.orm.panache.PanacheRepository; + +@ApplicationScoped +public class MockablePersonRepository implements PanacheRepository { + public List findOrdered() { + return find("ORDER BY name").list(); + } +} diff --git a/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/Person.java b/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/Person.java index 8042ebc531645..5afa058e8dbb1 100644 --- a/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/Person.java +++ b/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/Person.java @@ -41,7 +41,7 @@ public class Person extends PanacheEntity { @Transient public int serialisationTrick; - public static List findOrdered() { + public static List findOrdered() { return find("ORDER BY name").list(); } @@ -55,4 +55,8 @@ public int getSerialisationTrick() { public void setSerialisationTrick(int serialisationTrick) { this.serialisationTrick = serialisationTrick; } + + public static long methodWithPrimitiveParams(boolean b, byte bb, short s, int i, long l, float f, double d, char c) { + return 0; + } } diff --git a/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/PersonRepository.java b/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/PersonRepository.java index 65eaca454e303..805f66f4e3db9 100644 --- a/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/PersonRepository.java +++ b/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/PersonRepository.java @@ -1,9 +1,14 @@ package io.quarkus.it.panache; +import java.util.List; + import javax.enterprise.context.ApplicationScoped; import io.quarkus.hibernate.orm.panache.PanacheRepository; @ApplicationScoped public class PersonRepository implements PanacheRepository { + public List findOrdered() { + return find("ORDER BY name").list(); + } } diff --git a/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/TestEndpoint.java b/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/TestEndpoint.java index 3b51441e9a840..eca1f32696730 100644 --- a/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/TestEndpoint.java +++ b/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/TestEndpoint.java @@ -43,6 +43,10 @@ @Path("test") public class TestEndpoint { + // fake unused injection point to force ArC to not remove this otherwise I can't mock it in the tests + @Inject + MockablePersonRepository mockablePersonRepository; + @GET @Path("model") @Transactional 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 112ece19a4bee..add79fb5bd40f 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,12 +4,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import java.io.StringWriter; -import java.util.Optional; -import javax.inject.Inject; import javax.json.bind.Jsonb; import javax.json.bind.JsonbBuilder; -import javax.persistence.LockModeType; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; @@ -20,7 +17,6 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase; import io.quarkus.test.junit.DisabledOnNativeImage; import io.quarkus.test.junit.QuarkusTest; import io.restassured.RestAssured; @@ -167,35 +163,4 @@ public void testBug7721() { public void testBug8254() { RestAssured.when().get("/test/8254").then().body(is("OK")); } - - @Inject - PersonRepository realPersonRepository; - - @DisabledOnNativeImage - @Test - public void testPanacheRepositoryBridges() { - // normal method call - Assertions.assertNull(realPersonRepository.findById(0l)); - // bridge call - Assertions.assertNull(((PanacheRepositoryBase) realPersonRepository).findById(0l)); - // normal method call - Assertions.assertNull(realPersonRepository.findById(0l, LockModeType.NONE)); - // bridge call - Assertions.assertNull(((PanacheRepositoryBase) realPersonRepository).findById(0l, LockModeType.NONE)); - - // normal method call - Assertions.assertEquals(Optional.empty(), realPersonRepository.findByIdOptional(0l)); - // bridge call - Assertions.assertEquals(Optional.empty(), ((PanacheRepositoryBase) realPersonRepository).findByIdOptional(0l)); - // normal method call - Assertions.assertEquals(Optional.empty(), realPersonRepository.findByIdOptional(0l, LockModeType.NONE)); - // bridge call - Assertions.assertEquals(Optional.empty(), - ((PanacheRepositoryBase) realPersonRepository).findByIdOptional(0l, LockModeType.NONE)); - - // normal method call - Assertions.assertEquals(false, realPersonRepository.deleteById(0l)); - // bridge call - Assertions.assertEquals(false, ((PanacheRepositoryBase) realPersonRepository).deleteById(0l)); - } } diff --git a/integration-tests/hibernate-orm-panache/src/test/java/io/quarkus/it/panache/PanacheMockingTest.java b/integration-tests/hibernate-orm-panache/src/test/java/io/quarkus/it/panache/PanacheMockingTest.java new file mode 100644 index 0000000000000..722070bdc989b --- /dev/null +++ b/integration-tests/hibernate-orm-panache/src/test/java/io/quarkus/it/panache/PanacheMockingTest.java @@ -0,0 +1,136 @@ +package io.quarkus.it.panache; + +import java.util.Collections; +import java.util.Optional; + +import javax.inject.Inject; +import javax.persistence.LockModeType; +import javax.ws.rs.WebApplicationException; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase; +import io.quarkus.panache.mock.PanacheMock; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.mockito.InjectMock; + +@QuarkusTest +public class PanacheMockingTest { + + @Test + @Order(1) + 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()); + + PanacheMock.verify(Person.class, Mockito.times(4)).count(); + + Person p = new Person(); + 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()); + + PanacheMock.verify(Person.class).findOrdered(); + PanacheMock.verify(Person.class, Mockito.atLeastOnce()).findById(Mockito.any()); + PanacheMock.verifyNoMoreInteractions(Person.class); + + Assertions.assertEquals(0, Person.methodWithPrimitiveParams(true, (byte) 0, (short) 0, 0, 2, 2.0f, 2.0, 'c')); + } + + @Test + @Order(2) + public void testPanacheMockingWasCleared() { + Assertions.assertFalse(PanacheMock.IsMockEnabled); + } + + @InjectMock + MockablePersonRepository mockablePersonRepository; + + @Test + public void testPanacheRepositoryMocking() throws Throwable { + Assertions.assertEquals(0, mockablePersonRepository.count()); + + Mockito.when(mockablePersonRepository.count()).thenReturn(23l); + Assertions.assertEquals(23, mockablePersonRepository.count()); + + Mockito.when(mockablePersonRepository.count()).thenReturn(42l); + Assertions.assertEquals(42, mockablePersonRepository.count()); + + Mockito.when(mockablePersonRepository.count()).thenCallRealMethod(); + Assertions.assertEquals(0, mockablePersonRepository.count()); + + Mockito.verify(mockablePersonRepository, Mockito.times(4)).count(); + + Person p = new Person(); + Mockito.when(mockablePersonRepository.findById(12l)).thenReturn(p); + Assertions.assertSame(p, mockablePersonRepository.findById(12l)); + Assertions.assertNull(mockablePersonRepository.findById(42l)); + + Mockito.when(mockablePersonRepository.findById(12l)).thenThrow(new WebApplicationException()); + try { + mockablePersonRepository.findById(12l); + Assertions.fail(); + } catch (WebApplicationException x) { + } + + Mockito.when(mockablePersonRepository.findOrdered()).thenReturn(Collections.emptyList()); + Assertions.assertTrue(mockablePersonRepository.findOrdered().isEmpty()); + + Mockito.verify(mockablePersonRepository).findOrdered(); + Mockito.verify(mockablePersonRepository, Mockito.atLeastOnce()).findById(Mockito.any()); + Mockito.verifyNoMoreInteractions(mockablePersonRepository); + } + + @Inject + PersonRepository realPersonRepository; + + @Test + public void testPanacheRepositoryBridges() { + // normal method call + Assertions.assertNull(realPersonRepository.findById(0l)); + // bridge call + Assertions.assertNull(((PanacheRepositoryBase) realPersonRepository).findById(0l)); + // normal method call + Assertions.assertNull(realPersonRepository.findById(0l, LockModeType.NONE)); + // bridge call + Assertions.assertNull(((PanacheRepositoryBase) realPersonRepository).findById(0l, LockModeType.NONE)); + + // normal method call + Assertions.assertEquals(Optional.empty(), realPersonRepository.findByIdOptional(0l)); + // bridge call + Assertions.assertEquals(Optional.empty(), ((PanacheRepositoryBase) realPersonRepository).findByIdOptional(0l)); + // normal method call + Assertions.assertEquals(Optional.empty(), realPersonRepository.findByIdOptional(0l, LockModeType.NONE)); + // bridge call + Assertions.assertEquals(Optional.empty(), + ((PanacheRepositoryBase) realPersonRepository).findByIdOptional(0l, LockModeType.NONE)); + + // normal method call + Assertions.assertEquals(false, realPersonRepository.deleteById(0l)); + // bridge call + Assertions.assertEquals(false, ((PanacheRepositoryBase) realPersonRepository).deleteById(0l)); + } +} diff --git a/integration-tests/mongodb-panache/pom.xml b/integration-tests/mongodb-panache/pom.xml index c5be2c1c9709a..fe0c950cfb7ab 100755 --- a/integration-tests/mongodb-panache/pom.xml +++ b/integration-tests/mongodb-panache/pom.xml @@ -44,6 +44,11 @@ quarkus-junit5 test + + io.quarkus + quarkus-panache-mock + test + io.rest-assured rest-assured diff --git a/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/person/MockablePersonRepository.java b/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/person/MockablePersonRepository.java new file mode 100644 index 0000000000000..51d645b204e97 --- /dev/null +++ b/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/person/MockablePersonRepository.java @@ -0,0 +1,15 @@ +package io.quarkus.it.mongodb.panache.person; + +import java.util.List; + +import javax.enterprise.context.ApplicationScoped; + +import io.quarkus.mongodb.panache.PanacheMongoRepositoryBase; +import io.quarkus.panache.common.Sort; + +@ApplicationScoped +public class MockablePersonRepository implements PanacheMongoRepositoryBase { + public List findOrdered() { + return findAll(Sort.by("lastname", "firstname")).list(); + } +} diff --git a/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/person/PersonEntity.java b/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/person/PersonEntity.java index 4a146d9d9c1a5..be33769b45e73 100644 --- a/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/person/PersonEntity.java +++ b/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/person/PersonEntity.java @@ -1,12 +1,19 @@ package io.quarkus.it.mongodb.panache.person; +import java.util.List; + import org.bson.codecs.pojo.annotations.BsonId; import io.quarkus.mongodb.panache.PanacheMongoEntityBase; +import io.quarkus.panache.common.Sort; public class PersonEntity extends PanacheMongoEntityBase { @BsonId public Long id; public String firstname; public String lastname; + + public static List findOrdered() { + return findAll(Sort.by("lastname", "firstname")).list(); + } } diff --git a/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/person/PersonRepositoryResource.java b/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/person/PersonRepositoryResource.java index e84584094bc04..8c7e1a54e3741 100644 --- a/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/person/PersonRepositoryResource.java +++ b/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/person/PersonRepositoryResource.java @@ -15,6 +15,10 @@ @Consumes(MediaType.APPLICATION_JSON) public class PersonRepositoryResource { + // fake unused injection point to force ArC to not remove this otherwise I can't mock it in the tests + @Inject + MockablePersonRepository mockablePersonRepository; + @Inject PersonRepository personRepository; diff --git a/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/MongodbPanacheMockingTest.java b/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/MongodbPanacheMockingTest.java new file mode 100644 index 0000000000000..1f5d7ad5f72be --- /dev/null +++ b/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/MongodbPanacheMockingTest.java @@ -0,0 +1,131 @@ +package io.quarkus.it.mongodb.panache; + +import java.util.Collections; +import java.util.Optional; + +import javax.inject.Inject; +import javax.ws.rs.WebApplicationException; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import io.quarkus.it.mongodb.panache.person.MockablePersonRepository; +import io.quarkus.it.mongodb.panache.person.PersonEntity; +import io.quarkus.it.mongodb.panache.person.PersonRepository; +import io.quarkus.mongodb.panache.PanacheMongoRepositoryBase; +import io.quarkus.panache.mock.PanacheMock; +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.mockito.InjectMock; + +@QuarkusTest +@QuarkusTestResource(MongoTestResource.class) +public class MongodbPanacheMockingTest { + + @Test + @Order(1) + public void testPanacheMocking() { + PanacheMock.mock(PersonEntity.class); + + Assertions.assertEquals(0, PersonEntity.count()); + + Mockito.when(PersonEntity.count()).thenReturn(23l); + Assertions.assertEquals(23, PersonEntity.count()); + + Mockito.when(PersonEntity.count()).thenReturn(42l); + Assertions.assertEquals(42, PersonEntity.count()); + + Mockito.when(PersonEntity.count()).thenCallRealMethod(); + Assertions.assertEquals(0, PersonEntity.count()); + + PanacheMock.verify(PersonEntity.class, Mockito.times(4)).count(); + + PersonEntity p = new PersonEntity(); + + Mockito.when(PersonEntity.findById(12l)).thenReturn(p); + Assertions.assertSame(p, PersonEntity.findById(12l)); + Assertions.assertNull(PersonEntity.findById(42l)); + + Mockito.when(PersonEntity.findById(12l)).thenThrow(new WebApplicationException()); + try { + PersonEntity.findById(12l); + Assertions.fail(); + } catch (WebApplicationException x) { + } + + Mockito.when(PersonEntity.findOrdered()).thenReturn(Collections.emptyList()); + Assertions.assertTrue(PersonEntity.findOrdered().isEmpty()); + + PanacheMock.verify(PersonEntity.class).findOrdered(); + PanacheMock.verify(PersonEntity.class, Mockito.atLeastOnce()).findById(Mockito.any()); + PanacheMock.verifyNoMoreInteractions(PersonEntity.class); + } + + @Test + @Order(2) + public void testPanacheMockingWasCleared() { + Assertions.assertFalse(PanacheMock.IsMockEnabled); + } + + @InjectMock + MockablePersonRepository mockablePersonRepository; + + @Test + public void testPanacheRepositoryMocking() throws Throwable { + Assertions.assertEquals(0, mockablePersonRepository.count()); + + Mockito.when(mockablePersonRepository.count()).thenReturn(23l); + Assertions.assertEquals(23, mockablePersonRepository.count()); + + Mockito.when(mockablePersonRepository.count()).thenReturn(42l); + Assertions.assertEquals(42, mockablePersonRepository.count()); + + Mockito.when(mockablePersonRepository.count()).thenCallRealMethod(); + Assertions.assertEquals(0, mockablePersonRepository.count()); + + Mockito.verify(mockablePersonRepository, Mockito.times(4)).count(); + + PersonEntity p = new PersonEntity(); + Mockito.when(mockablePersonRepository.findById(12l)).thenReturn(p); + Assertions.assertSame(p, mockablePersonRepository.findById(12l)); + Assertions.assertNull(mockablePersonRepository.findById(42l)); + + Mockito.when(mockablePersonRepository.findById(12l)).thenThrow(new WebApplicationException()); + try { + mockablePersonRepository.findById(12l); + Assertions.fail(); + } catch (WebApplicationException x) { + } + + Mockito.when(mockablePersonRepository.findOrdered()).thenReturn(Collections.emptyList()); + Assertions.assertTrue(mockablePersonRepository.findOrdered().isEmpty()); + + Mockito.verify(mockablePersonRepository).findOrdered(); + Mockito.verify(mockablePersonRepository, Mockito.atLeastOnce()).findById(Mockito.any()); + Mockito.verifyNoMoreInteractions(mockablePersonRepository); + } + + @Inject + PersonRepository realPersonRepository; + + @Test + public void testPanacheRepositoryBridges() { + // normal method call + Assertions.assertNull(realPersonRepository.findById(0l)); + // bridge call + Assertions.assertNull(((PanacheMongoRepositoryBase) realPersonRepository).findById(0l)); + + // normal method call + Assertions.assertEquals(Optional.empty(), realPersonRepository.findByIdOptional(0l)); + // bridge call + Assertions.assertEquals(Optional.empty(), ((PanacheMongoRepositoryBase) realPersonRepository).findByIdOptional(0l)); + + // normal method call + Assertions.assertEquals(false, realPersonRepository.deleteById(0l)); + // bridge call + Assertions.assertEquals(false, ((PanacheMongoRepositoryBase) realPersonRepository).deleteById(0l)); + } + +} diff --git a/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/MongodbPanacheResourceTest.java b/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/MongodbPanacheResourceTest.java index a111e25f2704a..dedf714d3983f 100644 --- a/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/MongodbPanacheResourceTest.java +++ b/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/MongodbPanacheResourceTest.java @@ -10,9 +10,6 @@ import java.util.Date; import java.util.GregorianCalendar; import java.util.List; -import java.util.Optional; - -import javax.inject.Inject; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -24,10 +21,7 @@ import io.quarkus.it.mongodb.panache.book.BookDetail; import io.quarkus.it.mongodb.panache.person.Person; -import io.quarkus.it.mongodb.panache.person.PersonRepository; -import io.quarkus.mongodb.panache.PanacheMongoRepositoryBase; import io.quarkus.test.common.QuarkusTestResource; -import io.quarkus.test.junit.DisabledOnNativeImage; import io.quarkus.test.junit.QuarkusTest; import io.restassured.RestAssured; import io.restassured.common.mapper.TypeRef; @@ -360,26 +354,4 @@ public void testMoreEntityFunctionalities() { public void testMoreRepositoryFunctionalities() { get("/test/imperative/repository").then().statusCode(200); } - - @Inject - PersonRepository realPersonRepository; - - @DisabledOnNativeImage - @Test - public void testPanacheRepositoryBridges() { - // normal method call - Assertions.assertNull(realPersonRepository.findById(0l)); - // bridge call - Assertions.assertNull(((PanacheMongoRepositoryBase) realPersonRepository).findById(0l)); - - // normal method call - Assertions.assertEquals(Optional.empty(), realPersonRepository.findByIdOptional(0l)); - // bridge call - Assertions.assertEquals(Optional.empty(), ((PanacheMongoRepositoryBase) realPersonRepository).findByIdOptional(0l)); - - // normal method call - Assertions.assertEquals(false, realPersonRepository.deleteById(0l)); - // bridge call - Assertions.assertEquals(false, ((PanacheMongoRepositoryBase) realPersonRepository).deleteById(0l)); - } }