From e624fa382fd77c329af0ba2406c43e1dc8f35bc2 Mon Sep 17 00:00:00 2001 From: Benedikt Fein Date: Sat, 28 May 2022 01:59:05 +0200 Subject: [PATCH] Allow access to (package-)private and protected members (#213) Add additional methods to the ReflectionTestUtils to allow access to (package) private and protected attributes, constructors, and methods. --- .../in/test/api/util/ClassMemberAccessor.java | 214 ++++++++++++ .../in/test/api/util/ReflectionTestUtils.java | 316 ++++++++++++++++-- .../api/util/ClassMemberAccessorTest.java | 119 +++++++ .../tum/in/test/integration/DynamicsTest.java | 2 +- .../integration/ReflectionTestUtilsTest.java | 24 ++ .../testuser/ReflectionTestUtilsUser.java | 29 +- .../structural/AbstractClassExtension.java | 27 ++ .../subject/structural/SomeAbstractClass.java | 18 + .../subject/structural/SomeClass.java | 3 +- .../subpackage/SubpackageClass.java | 7 + 10 files changed, 722 insertions(+), 37 deletions(-) create mode 100644 src/main/java/de/tum/in/test/api/util/ClassMemberAccessor.java create mode 100644 src/test/java/de/tum/in/test/api/util/ClassMemberAccessorTest.java create mode 100644 src/test/java/de/tum/in/test/integration/testuser/subject/structural/AbstractClassExtension.java create mode 100644 src/test/java/de/tum/in/test/integration/testuser/subject/structural/subpackage/SubpackageClass.java diff --git a/src/main/java/de/tum/in/test/api/util/ClassMemberAccessor.java b/src/main/java/de/tum/in/test/api/util/ClassMemberAccessor.java new file mode 100644 index 00000000..4e4a5f2f --- /dev/null +++ b/src/main/java/de/tum/in/test/api/util/ClassMemberAccessor.java @@ -0,0 +1,214 @@ +package de.tum.in.test.api.util; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Stream; + +/** + * Provides utility methods to search for declared and inherited public, + * protected, and (package) private members of classes. + */ +class ClassMemberAccessor { + private ClassMemberAccessor() { + } + + /** + * Retrieve a method with arguments of a given class by its name. + *

+ * Also recursively searches for an inherited method in all superclasses and + * implemented interfaces. + * + * @param clazz The class that declares or inherits the method. + * @param methodName The name of the method. + * @param findNonPublic True, if this method should search for (package) + * private or protected (inherited) methods. + * @param parameterTypes The parameter types of this method. + * @return The wanted method. + * @throws NoSuchMethodException Thrown if the specified method cannot be found. + */ + static Method getMethod(Class clazz, String methodName, boolean findNonPublic, Class[] parameterTypes) + throws NoSuchMethodException { + if (findNonPublic) { + return getNonPublicMethod(clazz, methodName, parameterTypes); + } + try { + return clazz.getMethod(methodName, parameterTypes); + } catch (@SuppressWarnings("unused") NoSuchMethodException nsme) { + /* + * Also search for declared methods in the own class even when not explicitly + * searching for private methods to be able to provide error messages to the + * users that the method exists, but with the wrong visibility. + */ + return clazz.getDeclaredMethod(methodName, parameterTypes); + } + } + + /** + * Retrieve a method with arguments of a given class by its name. + *

+ * Also recursively searches for an inherited method in all superclasses and + * implemented interfaces. Finds a method even if it is not declared to be + * visible outside the declaring class. + * + * @param declaringClass The class that declares or inherits the method. + * @param methodName The name of the method. + * @param parameterTypes The parameter types of this method. + * @return The wanted method. + * @throws NoSuchMethodException Thrown if the specified method cannot be found. + */ + private static Method getNonPublicMethod(Class declaringClass, String methodName, Class[] parameterTypes) + throws NoSuchMethodException { + return getClassHierarchy(declaringClass).flatMap(c -> { + try { + return getInheritedMethod(declaringClass, c, methodName, parameterTypes).stream(); + } catch (@SuppressWarnings("unused") NoSuchMethodException nsme) { + return Stream.empty(); + } + }).findFirst().orElseThrow(() -> new NoSuchMethodException(methodName)); + } + + /** + * Searches for a method in the target class that might be inherited from the + * declaring class. + * + * @param targetClass The class in which the method should be accessible. + * @param declaringClass The class from which the method might be inherited. + * @param methodName The name of the method. + * @param parameterTypes The parameter types of the method. + * @return A method that is accessible in the target class. + * @throws NoSuchMethodException Thrown if no method as specified could be found + * in either class. + */ + private static Optional getInheritedMethod(Class targetClass, Class declaringClass, String methodName, + Class[] parameterTypes) throws NoSuchMethodException { + Method method = declaringClass.getDeclaredMethod(methodName, parameterTypes); + if (isInheritable(targetClass, declaringClass, method.getModifiers(), true)) { + return Optional.of(method); + } + return Optional.empty(); + } + + /** + * Retrieve a field of a given class by its name. + *

+ * Also recursively searches for an inherited field in all superclasses and + * implemented interfaces. Finds a field even if it is not declared to be + * visible outside the declaring class. + * + * @param clazz The class that declares or inherits the field. + * @param fieldName The name of the attribute. + * @param findNonPublic True, if this method should search for (package) private + * or protected (inherited) attributes. + * @return The wanted field. + * @throws NoSuchFieldException Thrown if the specified field cannot be found. + */ + static Field getField(Class clazz, String fieldName, boolean findNonPublic) throws NoSuchFieldException { + if (findNonPublic) { + return getNonPublicField(clazz, fieldName); + } + try { + return clazz.getField(fieldName); + } catch (@SuppressWarnings("unused") NoSuchFieldException nsfe) { + /* + * Also search for declared fields in the own class even when not explicitly + * searching for private fields to be able to provide error messages to the + * users that the field exists, but with the wrong visibility. + */ + return clazz.getDeclaredField(fieldName); + } + } + + /** + * Retrieve a field of a given class by its name. + *

+ * Also recursively searches for an inherited field in all superclasses and + * implemented interfaces. + * + * @param declaringClass The class that declares or inherits the field. + * @param fieldName The name of the attribute. + * @return The wanted field. + * @throws NoSuchFieldException Thrown if the specified field cannot be found. + */ + private static Field getNonPublicField(Class declaringClass, String fieldName) throws NoSuchFieldException { + return getClassHierarchy(declaringClass).flatMap(c -> { + try { + return getInheritedField(declaringClass, c, fieldName).stream(); + } catch (@SuppressWarnings("unused") NoSuchFieldException nsfe) { + return Stream.empty(); + } + }).findFirst().orElseThrow(() -> new NoSuchFieldException(fieldName)); + } + + /** + * Searches for a field in the target class that might be inherited from the + * declaring class. + * + * @param targetClass The class in which the field should be accessible. + * @param declaringClass The class from which the field might be inherited. + * @param fieldName The name of the attribute. + * @return A field that is accessible in the target class. + * @throws NoSuchFieldException Thrown if no field with the given name could be + * found in either class. + */ + private static Optional getInheritedField(Class targetClass, Class declaringClass, String fieldName) + throws NoSuchFieldException { + Field field = declaringClass.getDeclaredField(fieldName); + if (isInheritable(targetClass, declaringClass, field.getModifiers(), false)) { + return Optional.of(field); + } + return Optional.empty(); + } + + /** + * Searches for all classes and interfaces the given class is inheriting from. + *

+ * The given class itself is part of the result. Performs a depth-first search + * starting with classes, then interfaces. + * + * @param clazz The class for which the inheritance tree should be traversed. + * @return A stream of the given class and all superclasses and interfaces it + * inherits from. + */ + private static Stream> getClassHierarchy(Class clazz) { + Stream> directSuperclasses = Stream + .concat(Stream.of(clazz.getSuperclass()), Arrays.stream(clazz.getInterfaces())) + .filter(Objects::nonNull); + return Stream.concat(Stream.of(clazz), directSuperclasses.flatMap(ClassMemberAccessor::getClassHierarchy)); + } + + /** + * Checks if a field or method member of a class is accessible in a subclass. + * + * @param targetClass The class in which the class member of + * {@code declaredInClass} should be accessible. Assumes + * that {@code declaredInClass} is a superclass of + * {@code targetClass}. + * @param declaredInClass The class in which the member was declared. + * @param modifier The modifiers of the member. + * @param isMethod True, if the inheritance check should be performed for + * a method. + * @return True, if the class member of {@code declaredInClass} is accessible in + * its subclass {@code targetClass}. + */ + private static boolean isInheritable(Class targetClass, Class declaredInClass, int modifier, + boolean isMethod) { + if (targetClass.equals(declaredInClass)) { + return true; + } + if (isMethod && declaredInClass.isInterface() && Modifier.isStatic(modifier)) { + /* + * Static methods are not inherited from interfaces. Interface attributes are + * implicitly declared static, too, but follow the usual inheritance rules. + */ + return false; + } + boolean isInheritable = Modifier.isProtected(modifier) || Modifier.isPublic(modifier); + boolean isInheritableInPackage = !Modifier.isPrivate(modifier) + && targetClass.getPackage().equals(declaredInClass.getPackage()); + return isInheritable || isInheritableInPackage; + } +} diff --git a/src/main/java/de/tum/in/test/api/util/ReflectionTestUtils.java b/src/main/java/de/tum/in/test/api/util/ReflectionTestUtils.java index 68cc0594..d148dc7a 100644 --- a/src/main/java/de/tum/in/test/api/util/ReflectionTestUtils.java +++ b/src/main/java/de/tum/in/test/api/util/ReflectionTestUtils.java @@ -3,6 +3,7 @@ import static de.tum.in.test.api.localization.Messages.*; import java.lang.reflect.Constructor; +import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Arrays; @@ -33,7 +34,7 @@ * * * @author Stephan Krusche (krusche@in.tum.de) - * @version 5.1 (2022-03-30) + * @version 6.0 (2022-05-27) */ @API(status = Status.STABLE) public final class ReflectionTestUtils { @@ -73,8 +74,6 @@ public static Class getClazz(String qualifiedClassName) { * retrieved (package.classname) * @param constructorArgs Parameter instances of the constructor of the * class, that it should use to get instantiated with. - * Do not include, if the constructor has no - * arguments. * @return The instance of this class. * @see #newInstance(Class, Object...) */ @@ -82,6 +81,30 @@ public static Object newInstance(String qualifiedClassName, Object... constructo return newInstance(getClazz(qualifiedClassName), constructorArgs); } + /** + * Instantiate an object of a class by its qualified name and the constructor + * arguments, if applicable. + *

+ * This method does not support passing null, passing subclasses of the + * parameter types or invoking constructors with primitive parameters. Use + * {@link #newInstanceFromNonPublicConstructor(Constructor, Object...)} for + * that. + *

+ * Forces the access to package-private, {@code protected}, and {@code private} + * constructors. Use {@link #newInstance(String, Object...)} if you do not + * require this functionality. + * + * @param qualifiedClassName The qualified name of the class that needs to get + * retrieved (package.classname) + * @param constructorArgs Parameter instances of the constructor of the class + * that it should use to get instantiated with. + * @return The instance of this class. + * @see #newInstance(Class, Object...) + */ + public static Object newInstanceFromNonPublicConstructor(String qualifiedClassName, Object... constructorArgs) { + return newInstanceFromNonPublicConstructor(getClazz(qualifiedClassName), constructorArgs); + } + /** * Instantiate an object of a given class using the given constructor arguments, * if applicable. @@ -92,16 +115,84 @@ public static Object newInstance(String qualifiedClassName, Object... constructo * * @param clazz The class for which a new instance should be created * @param constructorArgs Parameter instances of the constructor of the class, - * that it should use to get instantiated with. Do not - * include, if the constructor has no arguments. + * that it should use to get instantiated with. * @return The instance of this class. */ public static Object newInstance(Class clazz, Object... constructorArgs) { + return newInstanceAccessible(clazz, false, constructorArgs); + } + + /** + * Instantiate an object of a given class using the given constructor arguments, + * if applicable. + *

+ * This method does not support passing null, passing subclasses of the + * parameter types or invoking constructors with primitive parameters. Use + * {@link #newInstance(Constructor, Object[])} for that. + *

+ * Forces the access to package-private, {@code protected}, and {@code private} + * constructors. Use {@link #newInstance(Class, Object...)} if you do not + * require this functionality. + * + * @param clazz The class for which a new instance should be created + * @param constructorArgs Parameter instances of the constructor of the class, + * that it should use to get instantiated with. + * @return The instance of this class. + */ + public static Object newInstanceFromNonPublicConstructor(Class clazz, Object... constructorArgs) { + return newInstanceAccessible(clazz, true, constructorArgs); + } + + /** + * Instantiate an object of a class by using a specific constructor and + * constructor arguments, if applicable. + * + * @param constructor The actual constructor that should be used for + * creating a new instance of the object + * @param constructorArgs Parameter instances of the constructor of the class, + * that it should use to get instantiated with. + * @return The instance of this class. + */ + public static Object newInstance(Constructor constructor, Object... constructorArgs) { + return newInstanceAccessible(constructor, false, constructorArgs); + } + + /** + * Instantiate an object of a class by using a specific constructor and + * constructor arguments, if applicable. + *

+ * Forces the access to package-private, {@code protected}, and {@code private} + * constructors. Use {@link #newInstance(Constructor, Object...)} if you do not + * require this functionality. + * + * @param constructor The actual constructor that should be used for + * creating a new instance of the object + * @param constructorArgs Parameter instances of the constructor of the class, + * that it should use to get instantiated with. + * @return The instance of this class. + */ + public static Object newInstanceFromNonPublicConstructor(Constructor constructor, Object... constructorArgs) { + return newInstanceAccessible(constructor, true, constructorArgs); + } + + /** + * Instantiate an object of a class by using a specific constructor and + * constructor arguments, if applicable. + * + * @param clazz The type of the class that should be instantiated. + * @param forceAccess True, if access to a (package) private or protected + * constructor should be forced. Might fail with an + * {@link IllegalAccessException} otherwise. + * @param constructorArgs Parameter instances the constructor should be called + * with. + * @return An instance of the given class type. + */ + private static Object newInstanceAccessible(Class clazz, boolean forceAccess, Object[] constructorArgs) { var constructorArgTypes = getParameterTypes(constructorArgs, "reflection_test_utils.constructor_null_args", //$NON-NLS-1$ clazz.getSimpleName()); try { Constructor constructor = clazz.getDeclaredConstructor(constructorArgTypes); - return newInstance(constructor, constructorArgs); + return newInstanceAccessible(constructor, forceAccess, constructorArgs); } catch (@SuppressWarnings("unused") NoSuchMethodException nsme) { throw localizedFailure("reflection_test_utils.constructor_not_found_args", clazz.getSimpleName(), //$NON-NLS-1$ getParameterTypesAsString(constructorArgTypes)); @@ -112,15 +203,21 @@ public static Object newInstance(Class clazz, Object... constructorArgs) { * Instantiate an object of a class by using a specific constructor and * constructor arguments, if applicable. * - * @param constructor The actual constructor that should be used for - * creating a new instance of the object - * @param constructorArgs Parameter instances of the constructor of the class, - * that it should use to get instantiated with. Do not - * include, if the constructor has no arguments. - * @return The instance of this class. + * @param constructor The constructor that should be used to instantiate an + * object. + * @param forceAccess True, if access to a (package) private or protected + * constructor should be forced. Might fail with an + * {@link IllegalAccessException} otherwise. + * @param constructorArgs Parameter instances the constructor should be called + * with. + * @return The object created by calling the given constructor. */ - public static Object newInstance(Constructor constructor, Object... constructorArgs) { + private static Object newInstanceAccessible(Constructor constructor, boolean forceAccess, + Object[] constructorArgs) { try { + if (forceAccess) { + constructor.setAccessible(true); + } return constructor.newInstance(constructorArgs); } catch (@SuppressWarnings("unused") IllegalAccessException iae) { throw localizedFailure("reflection_test_utils.constructor_access", //$NON-NLS-1$ @@ -153,9 +250,46 @@ public static Object newInstance(Constructor constructor, Object... construct * @return The instance of the attribute with the wanted value. */ public static Object valueForAttribute(Object object, String attributeName) { + return valueForAttribute(object, attributeName, false); + } + + /** + * Retrieve an attribute value of a given instance of a class by the attribute + * name. + *

+ * Forces access to package-private, {@code protected}, and {@code private} + * attributes. Use {@link #valueForAttribute(Object, String)} when reading + * accessible attributes. + * + * @param object The instance of the class that contains the attribute. + * Must not be null, even for static fields. + * @param attributeName The name of the attribute whose value needs to get + * retrieved. + * @return The instance of the attribute with the wanted value. + */ + public static Object valueForNonPublicAttribute(Object object, String attributeName) { + return valueForAttribute(object, attributeName, true); + } + + /** + * Retrieve an attribute value of a given instance of a class by the attribute + * name. + * + * @param object The object from which the attribute should be read. + * @param attributeName The name of the attribute that should be read. + * @param forceAccess True, if access to a (package) private or protected + * attribute should be forced. Might fail with an + * {@link IllegalAccessException} otherwise. + * @return The value that is stored in the attribute in the given object. + */ + private static Object valueForAttribute(Object object, String attributeName, boolean forceAccess) { requireNonNull(object, "reflection_test_utils.attribute_null", attributeName); //$NON-NLS-1$ try { - return object.getClass().getDeclaredField(attributeName).get(object); + Field field = ClassMemberAccessor.getField(object.getClass(), attributeName, forceAccess); + if (forceAccess) { + field.setAccessible(true); + } + return field.get(object); } catch (@SuppressWarnings("unused") NoSuchFieldException nsfe) { throw localizedFailure("reflection_test_utils.attribute_not_found", attributeName, //$NON-NLS-1$ object.getClass().getSimpleName()); @@ -171,8 +305,7 @@ public static Object valueForAttribute(Object object, String attributeName) { * * @param object instance of the class that defines the method. * @param methodName the name of the method. - * @param parameterTypes The parameter types of this method. Do not include if - * the method has no parameters. + * @param parameterTypes The parameter types of this method. * @return The wanted method. */ public static Method getMethod(Object object, String methodName, Class... parameterTypes) { @@ -185,13 +318,27 @@ public static Method getMethod(Object object, String methodName, Class... par * * @param declaringClass The class that declares this method. * @param methodName The name of this method. - * @param parameterTypes The parameter types of this method. Do not include if - * the method has no parameters. + * @param parameterTypes The parameter types of this method. * @return The wanted method. */ public static Method getMethod(Class declaringClass, String methodName, Class... parameterTypes) { + return getMethodAccessible(declaringClass, methodName, false, parameterTypes); + } + + /** + * Retrieve a method with arguments of a given class by its name. + * + * @param declaringClass The class that declares this method. + * @param methodName The name of this method. + * @param findNonPublic True, if this method should search for (package) + * private or protected methods. + * @param parameterTypes The parameter types of this method. + * @return The wanted method. + */ + private static Method getMethodAccessible(Class declaringClass, String methodName, boolean findNonPublic, + Class[] parameterTypes) { try { - return declaringClass.getMethod(methodName, parameterTypes); + return ClassMemberAccessor.getMethod(declaringClass, methodName, findNonPublic, parameterTypes); } catch (@SuppressWarnings("unused") NoSuchMethodException nsme) { throw localizedFailure("reflection_test_utils.method_not_found", methodName, //$NON-NLS-1$ describeParameters(parameterTypes), declaringClass.getSimpleName()); @@ -212,14 +359,34 @@ public static Method getMethod(Class declaringClass, String methodName, Class * @param object The instance of the class that should invoke the method. * Must not be null, even for static methods. * @param methodName The method name that has to get invoked. - * @param params Parameter instances of the method. Do not include if the - * method has no parameters. + * @param params Parameter instances of the method. * @return The return value of the method. */ public static Object invokeMethod(Object object, String methodName, Object... params) { - var parameterTypes = getParameterTypes(params, "reflection_test_utils.method_null_args", methodName); //$NON-NLS-1$ - var method = getMethod(object, methodName, parameterTypes); - return invokeMethod(object, method, params); + return invokeMethodAccessible(object, methodName, false, params); + } + + /** + * Invoke a given method name of a given object with instances of the + * parameters. + *

+ * This method does not support invoking static methods and passing null, + * passing subclasses of the parameter types or invoking methods with primitive + * parameters. Use {@link #invokeNonPublicMethod(Object, Method, Object...)} for + * that. + *

+ * Forces access to package-private, {@code protected}, and {@code private} + * methods. Use {@link #invokeMethod(Object, String, Object...)} when invoking + * accessible methods. + * + * @param object The instance of the class that should invoke the method. + * Must not be null, even for static methods. + * @param methodName The method name that has to get invoked. + * @param params Parameter instances of the method. + * @return The return value of the method. + */ + public static Object invokeNonPublicMethod(Object object, String methodName, Object... params) { + return invokeMethodAccessible(object, methodName, true, params); } /** @@ -228,14 +395,63 @@ public static Object invokeMethod(Object object, String methodName, Object... pa * @param object The instance of the class that should invoke the method. Can be * null if the method is static. * @param method The method that has to get invoked. - * @param params Parameter instances of the method. Do not include if the method - * has no parameters. + * @param params Parameter instances of the method. * @return The return value of the method. */ public static Object invokeMethod(Object object, Method method, Object... params) { + return invokeMethodAccessible(object, method, false, params); + } + + /** + * Invoke a given method of a given object with instances of the parameters. + *

+ * Forces access to package-private, {@code protected}, and {@code private} + * methods. Use {@link #invokeMethod(Object, Method, Object...)} when invoking + * accessible methods. + * + * @param object The instance of the class that should invoke the method. Can be + * null if the method is static. + * @param method The method that has to get invoked. + * @param params Parameter instances of the method. + * @return The return value of the method. + */ + public static Object invokeNonPublicMethod(Object object, Method method, Object... params) { + return invokeMethodAccessible(object, method, true, params); + } + + /** + * Invoke a given method of a given object with instances of the parameters. + * + * @param object The instance of the class that should invoke the method. + * @param methodName The name of the method that has to get invoked. + * @param forceAccess True, if access to a (package) private or protected method + * should be forced. Might fail with an + * {@link IllegalAccessException} otherwise. + * @param params Parameter instances of the method. + * @return The return value of the method. + */ + private static Object invokeMethodAccessible(Object object, String methodName, boolean forceAccess, + Object[] params) { + var parameterTypes = getParameterTypes(params, "reflection_test_utils.method_null_args", methodName); //$NON-NLS-1$ + var method = getMethodAccessible(object.getClass(), methodName, forceAccess, parameterTypes); + return invokeMethodAccessible(object, method, forceAccess, params); + } + + /** + * Invoke a given method of a given object with instances of the parameters. + * + * @param object The instance of the class that should invoke the method. + * @param method The method that has to get invoked. + * @param forceAccess True, if access to a (package) private or protected method + * should be forced. Might fail with an + * {@link IllegalAccessException} otherwise. + * @param params Parameter instances of the method. + * @return The return value of the method. + */ + private static Object invokeMethodAccessible(Object object, Method method, boolean forceAccess, Object[] params) { // NOTE: object can be null, if method is static try { - return invokeMethodRethrowing(object, method, params); + return invokeMethodRethrowingAccessible(object, method, forceAccess, params); } catch (AssertionFailedError e) { throw e; } catch (Throwable e) { @@ -250,14 +466,53 @@ public static Object invokeMethod(Object object, Method method, Object... params * * @param object The instance of the class that should invoke the method. * @param method The method that has to get invoked. - * @param params Parameter instances of the method. Do not include if the method - * has no parameters. + * @param params Parameter instances of the method. * @throws Throwable the exception that was caught and which will be rethrown * @return The return value of the method. */ public static Object invokeMethodRethrowing(Object object, Method method, Object... params) throws Throwable { + return invokeMethodRethrowingAccessible(object, method, false, params); + } + + /** + * Invoke a given method of a given object with instances of the parameters, and + * rethrow an exception if one occurs during the method execution. + *

+ * Forces access to package-private, {@code protected}, and {@code private} + * methods. Use {@link #invokeMethodRethrowing(Object, Method, Object...)} when + * invoking accessible methods. + * + * @param object The instance of the class that should invoke the method. + * @param method The method that has to get invoked. + * @param params Parameter instances of the method. + * @throws Throwable the exception that was caught and which will be rethrown + * @return The return value of the method. + */ + public static Object invokeNonPublicMethodRethrowing(Object object, Method method, Object... params) + throws Throwable { + return invokeMethodRethrowingAccessible(object, method, true, params); + } + + /** + * Invoke a given method of a given object with instances of the parameters, and + * rethrow an exception if one occurs during the method execution. + * + * @param object The instance of the class that should invoke the method. + * @param method The method that has to get invoked. + * @param forceAccess True, if access to a (package) private or protected method + * should be forced. Might fail with an + * {@link IllegalAccessException} otherwise. + * @param params Parameter instances of the method. + * @throws Throwable the exception that was caught and which will be rethrown + * @return The return value of the method. + */ + private static Object invokeMethodRethrowingAccessible(Object object, Method method, boolean forceAccess, + Object[] params) throws Throwable { // NOTE: object can be null, if method is static try { + if (forceAccess) { + method.setAccessible(true); + } return method.invoke(object, params); } catch (@SuppressWarnings("unused") IllegalAccessException iae) { throw localizedFailure("reflection_test_utils.method_access", method.getName(), //$NON-NLS-1$ @@ -280,8 +535,7 @@ public static Object invokeMethodRethrowing(Object object, Method method, Object * Retrieve a constructor with arguments of a given class. * * @param declaringClass The class that declares this constructor. - * @param parameterTypes The parameter types of this method. Do not include if - * the method has no parameters. + * @param parameterTypes The parameter types of this method. * @param The type parameter of the constructor and class * @return The wanted method. */ diff --git a/src/test/java/de/tum/in/test/api/util/ClassMemberAccessorTest.java b/src/test/java/de/tum/in/test/api/util/ClassMemberAccessorTest.java new file mode 100644 index 00000000..54cdbd70 --- /dev/null +++ b/src/test/java/de/tum/in/test/api/util/ClassMemberAccessorTest.java @@ -0,0 +1,119 @@ +package de.tum.in.test.api.util; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertThrows; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import de.tum.in.test.integration.testuser.subject.structural.AbstractClassExtension; +import de.tum.in.test.integration.testuser.subject.structural.SomeAbstractClass; +import de.tum.in.test.integration.testuser.subject.structural.SomeClass; +import de.tum.in.test.integration.testuser.subject.structural.SomeInterface; +import de.tum.in.test.integration.testuser.subject.structural.subpackage.SubpackageClass; + +class ClassMemberAccessorTest { + + @ParameterizedTest + @ValueSource(booleans = { true, false }) + void getInheritedInterfaceDefaultMethod(boolean findNonPublic) throws NoSuchMethodException { + Method method = ClassMemberAccessor.getMethod(AbstractClassExtension.class, "doSomethingElse", findNonPublic, + new Class[] { int.class }); + assertThat(method.getDeclaringClass()).isEqualTo(SomeInterface.class); + } + + @ParameterizedTest + @ValueSource(booleans = { true, false }) + void getInterfaceAttribute(boolean findNonPublic) throws NoSuchFieldException { + Field field = ClassMemberAccessor.getField(AbstractClassExtension.class, "ANOTHER_CONSTANT", findNonPublic); + assertThat(field.getDeclaringClass()).isEqualTo(SomeInterface.class); + } + + @Test + void getMethodMatchingParameters() throws NoSuchMethodException { + Method method = ClassMemberAccessor.getMethod(AbstractClassExtension.class, "declaredMethod", true, + new Class[] { int.class }); + assertThat(Modifier.isPrivate(method.getModifiers())).isTrue(); + assertThat(method.getParameterTypes()).containsExactly(int.class); + } + + @ParameterizedTest + @ValueSource(booleans = { true, false }) + void getNonAccessiblePrivateSuperclassField(boolean findNonPublic) { + assertThrows(NoSuchFieldException.class, () -> ClassMemberAccessor.getField(AbstractClassExtension.class, + "somePrivateAttribute", findNonPublic)); + } + + @ParameterizedTest + @ValueSource(booleans = { true, false }) + void getNonAccessiblePrivateSuperclassMethod(boolean findNonPublic) { + assertThrows(NoSuchMethodException.class, () -> ClassMemberAccessor.getMethod(AbstractClassExtension.class, + "nonAbstractPrivate", findNonPublic, new Class[] {})); + } + + @ParameterizedTest + @ValueSource(booleans = { true, false }) + void getPackagePrivateMethodNoAccessInSubpackage(boolean findNonPublic) { + assertThrows(NoSuchMethodException.class, () -> ClassMemberAccessor.getMethod(SubpackageClass.class, + "nonAbstractPackagePrivate", findNonPublic, new Class[] {})); + } + + @Test + void getProtectedInheritedAttribute() throws NoSuchFieldException { + Field field = ClassMemberAccessor.getField(AbstractClassExtension.class, "someProtectedAttribute", true); + assertThat(field.getDeclaringClass()).isEqualTo(SomeAbstractClass.class); + } + + @Test + void getProtectedInheritedAttributeNoForcedAccess() { + assertThrows(NoSuchFieldException.class, + () -> ClassMemberAccessor.getField(AbstractClassExtension.class, "someProtectedAttribute", false)); + } + + @Test + void getProtectedInheritedMethod() throws NoSuchMethodException { + Method method = ClassMemberAccessor.getMethod(AbstractClassExtension.class, "nonAbstractProtected", true, + new Class[] {}); + assertThat(method).isNotNull(); + } + + @Test + void getProtectedInheritedMethodNoForcedAccess() { + assertThrows(NoSuchMethodException.class, () -> ClassMemberAccessor.getMethod(AbstractClassExtension.class, + "nonAbstractProtected", false, new Class[] {})); + } + + @Test + void getProtectedMethodAccessInSubpackage() throws NoSuchMethodException { + Method method = ClassMemberAccessor.getMethod(SubpackageClass.class, "nonAbstractProtected", true, + new Class[] {}); + assertThat(method).isNotNull(); + } + + @ParameterizedTest + @ValueSource(booleans = { true, false }) + void getPublicInheritedAttribute(boolean findNonPublic) throws NoSuchFieldException { + Field field = ClassMemberAccessor.getField(AbstractClassExtension.class, "someInt", findNonPublic); + assertThat(field.getDeclaringClass()).isEqualTo(SomeAbstractClass.class); + } + + @ParameterizedTest + @ValueSource(booleans = { true, false }) + void getPublicMethod(boolean findNonPublic) throws NoSuchMethodException { + Method method = ClassMemberAccessor.getMethod(AbstractClassExtension.class, "declaredMethod", findNonPublic, + new Class[] {}); + assertThat(method).isNotNull(); + } + + @ParameterizedTest + @ValueSource(booleans = { true, false }) + void getStaticMethodFromInterface(boolean findNonPublic) { + assertThrows(NoSuchMethodException.class, + () -> ClassMemberAccessor.getMethod(SomeClass.class, "getOne", findNonPublic, new Class[] {})); + } +} diff --git a/src/test/java/de/tum/in/test/integration/DynamicsTest.java b/src/test/java/de/tum/in/test/integration/DynamicsTest.java index 4d46d31e..04a93c6d 100644 --- a/src/test/java/de/tum/in/test/integration/DynamicsTest.java +++ b/src/test/java/de/tum/in/test/integration/DynamicsTest.java @@ -89,7 +89,7 @@ void test_class_searchNonPrivateFields() { @TestTest void test_class_searchPublicOrProtectedMethods() { tests.assertThatEvents().haveExactly(1, testFailedWith(class_searchPublicOrProtectedMethods, - AssertionFailedError.class, "Methode doSomething(java.lang.String) darf nicht public sein.")); + AssertionFailedError.class, "Methode nonAbstractProtected() darf nicht protected sein.")); } @TestTest diff --git a/src/test/java/de/tum/in/test/integration/ReflectionTestUtilsTest.java b/src/test/java/de/tum/in/test/integration/ReflectionTestUtilsTest.java index a0fb3df9..b4ca4c4c 100644 --- a/src/test/java/de/tum/in/test/integration/ReflectionTestUtilsTest.java +++ b/src/test/java/de/tum/in/test/integration/ReflectionTestUtilsTest.java @@ -27,6 +27,8 @@ class ReflectionTestUtilsTest { private final String testInvokeMethodRethrowing_illegalAccess = "testInvokeMethodRethrowing_illegalAccess"; private final String testInvokeMethodRethrowing_illegalArgument = "testInvokeMethodRethrowing_illegalArgument"; private final String testInvokeMethodRethrowing_nullPointer = "testInvokeMethodRethrowing_nullPointer"; + private final String testInvokePrivateMethodByName_success = "testInvokePrivateMethodByName_success"; + private final String testInvokePrivateMethodRethrowing_success = "testInvokePrivateMethodRethrowing_success"; private final String testNewInstance_classNotFound = "testNewInstance_classNotFound"; private final String testNewInstance_exceptionInInitializer = "testNewInstance_exceptionInInitializer"; private final String testNewInstance_illegalAccess = "testNewInstance_illegalAccess"; @@ -35,15 +37,22 @@ class ReflectionTestUtilsTest { private final String testNewInstance_invocationTarget = "testNewInstance_invocationTarget"; private final String testNewInstance_noSuchMethod = "testNewInstance_noSuchMethod"; private final String testNewInstance_success = "testNewInstance_success"; + private final String testNewInstancePrivateConstructor_success = "testNewInstancePrivateConstructor_success"; private final String testValueForAttribute_illegalAccess = "testValueForAttribute_illegalAccess"; private final String testValueForAttribute_noSuchField = "testValueForAttribute_noSuchField"; private final String testValueForAttribute_success = "testValueForAttribute_success"; + private final String testValueForPrivateAttribute_success = "testValueForPrivateAttribute_success"; @TestTest void test_invokeMethod_success() { tests.assertThatEvents().haveExactly(1, finishedSuccessfully(testInvokeMethod_success)); } + @TestTest + void test_invokePrivateMethodByName_success() { + tests.assertThatEvents().haveExactly(1, finishedSuccessfully(testInvokePrivateMethodByName_success)); + } + @TestTest void test_testGetConstructor_noSuchMethod() { tests.assertThatEvents().haveExactly(1, testFailedWith(testGetConstructor_noSuchMethod, @@ -109,6 +118,11 @@ void test_testInvokeMethodRethrowing_nullPointer() { "Could not invoke the method 'getAnotherAttribute' in the class SomeClass because the object was null and the method is an instance method. Make sure to check the static modifier of the method.")); } + @TestTest + void test_testInvokePrivateMethodRethrowing_success() { + tests.assertThatEvents().haveExactly(1, finishedSuccessfully(testInvokePrivateMethodRethrowing_success)); + } + @TestTest void test_testNewInstance_classNotFound() { tests.assertThatEvents().haveExactly(1, testFailedWith(testNewInstance_classNotFound, @@ -162,6 +176,11 @@ void test_testNewInstance_success() { tests.assertThatEvents().haveExactly(1, finishedSuccessfully(testNewInstance_success)); } + @TestTest + void test_testNewInstancePrivateConstructor_success() { + tests.assertThatEvents().haveExactly(1, finishedSuccessfully(testNewInstancePrivateConstructor_success)); + } + @TestTest void test_testValueForAttribute_illegalAccess() { tests.assertThatEvents().haveExactly(1, testFailedWith(testValueForAttribute_illegalAccess, @@ -180,4 +199,9 @@ void test_testValueForAttribute_noSuchField() { void test_testValueForAttribute_success() { tests.assertThatEvents().haveExactly(1, finishedSuccessfully(testValueForAttribute_success)); } + + @TestTest + void test_testValueForPrivateAttribute_success() { + tests.assertThatEvents().haveExactly(1, finishedSuccessfully(testValueForPrivateAttribute_success)); + } } diff --git a/src/test/java/de/tum/in/test/integration/testuser/ReflectionTestUtilsUser.java b/src/test/java/de/tum/in/test/integration/testuser/ReflectionTestUtilsUser.java index 101d4e93..0647b5ae 100644 --- a/src/test/java/de/tum/in/test/integration/testuser/ReflectionTestUtilsUser.java +++ b/src/test/java/de/tum/in/test/integration/testuser/ReflectionTestUtilsUser.java @@ -44,13 +44,13 @@ void testGetMethod_noMethodName() { } @Test - void testGetMethod_noSuchMethod_withParameters() { - getMethod(CLASS_INSTANCE, "someMethod", String.class); + void testGetMethod_noSuchMethod_noParameters() { + getMethod(CLASS_INSTANCE, "someMethod"); } @Test - void testGetMethod_noSuchMethod_noParameters() { - getMethod(CLASS_INSTANCE, "someMethod"); + void testGetMethod_noSuchMethod_withParameters() { + getMethod(CLASS_INSTANCE, "someMethod", String.class); } @Test @@ -88,6 +88,17 @@ void testInvokeMethodRethrowing_nullPointer() throws NoSuchMethodException { invokeMethod(null, method); } + @Test + void testInvokePrivateMethodByName_success() { + invokeNonPublicMethod(CLASS_INSTANCE, "superSecretMethod"); + } + + @Test + void testInvokePrivateMethodRethrowing_success() throws NoSuchMethodException { + var privateMethod = CLASS_INSTANCE.getClass().getDeclaredMethod("superSecretMethod"); + invokeNonPublicMethod(CLASS_INSTANCE, privateMethod); + } + @Test void testNewInstance_classNotFound() { newInstance("DoesNotExist"); @@ -130,6 +141,11 @@ void testNewInstance_success() { assertThat(instance).isInstanceOf(SomeClass.class); } + @Test + void testNewInstancePrivateConstructor_success() { + newInstanceFromNonPublicConstructor(CLASS_NAME, ""); + } + @Test void testValueForAttribute_illegalAccess() { valueForAttribute(CLASS_INSTANCE, "someAttribute"); @@ -145,4 +161,9 @@ void testValueForAttribute_success() { var value = valueForAttribute(CLASS_INSTANCE, "SOME_CONSTANT"); assertThat(value).isEqualTo(SomeClass.SOME_CONSTANT); } + + @Test + void testValueForPrivateAttribute_success() { + valueForNonPublicAttribute(CLASS_INSTANCE, "someAttribute"); + } } diff --git a/src/test/java/de/tum/in/test/integration/testuser/subject/structural/AbstractClassExtension.java b/src/test/java/de/tum/in/test/integration/testuser/subject/structural/AbstractClassExtension.java new file mode 100644 index 00000000..47212dac --- /dev/null +++ b/src/test/java/de/tum/in/test/integration/testuser/subject/structural/AbstractClassExtension.java @@ -0,0 +1,27 @@ +package de.tum.in.test.integration.testuser.subject.structural; + +public class AbstractClassExtension extends SomeAbstractClass implements SomeInterface { + @Override + void doNothing() { + // nothing + } + + public void declaredMethod() { + // nothing + } + + @SuppressWarnings("unused") + private void declaredMethod(int x) { + // nothing + } + + @SuppressWarnings("unused") + private void declaredMethod(String s) { + // nothing + } + + @Override + public int doSomething(String someString) { + return 10; + } +} diff --git a/src/test/java/de/tum/in/test/integration/testuser/subject/structural/SomeAbstractClass.java b/src/test/java/de/tum/in/test/integration/testuser/subject/structural/SomeAbstractClass.java index f19941ab..1de89e70 100644 --- a/src/test/java/de/tum/in/test/integration/testuser/subject/structural/SomeAbstractClass.java +++ b/src/test/java/de/tum/in/test/integration/testuser/subject/structural/SomeAbstractClass.java @@ -4,6 +4,11 @@ public abstract class SomeAbstractClass { public static int someInt = 2; + protected final String someProtectedAttribute = "hidden"; + + @SuppressWarnings("unused") + private final long somePrivateAttribute = 3L; + public SomeAbstractClass() { // nothing } @@ -14,4 +19,17 @@ protected SomeAbstractClass(String someString, int someInt) { } abstract void doNothing(); + + protected void nonAbstractProtected() { + // nothing + } + + @SuppressWarnings("unused") + private void nonAbstractPrivate() { + // nothing + } + + void nonAbstractPackagePrivate() { + // nothing + } } diff --git a/src/test/java/de/tum/in/test/integration/testuser/subject/structural/SomeClass.java b/src/test/java/de/tum/in/test/integration/testuser/subject/structural/SomeClass.java index d8a044d8..09029005 100644 --- a/src/test/java/de/tum/in/test/integration/testuser/subject/structural/SomeClass.java +++ b/src/test/java/de/tum/in/test/integration/testuser/subject/structural/SomeClass.java @@ -66,6 +66,7 @@ public Class initializeFailingClass() throws ClassNotFoundException { } private int superSecretMethod() { - return ThreadLocalRandom.current().nextInt(doSomethingOperations.size()); + return ThreadLocalRandom.current() + .nextInt(doSomethingOperations == null ? SOME_CONSTANT : doSomethingOperations.size()); } } diff --git a/src/test/java/de/tum/in/test/integration/testuser/subject/structural/subpackage/SubpackageClass.java b/src/test/java/de/tum/in/test/integration/testuser/subject/structural/subpackage/SubpackageClass.java new file mode 100644 index 00000000..e9c3bf8a --- /dev/null +++ b/src/test/java/de/tum/in/test/integration/testuser/subject/structural/subpackage/SubpackageClass.java @@ -0,0 +1,7 @@ +package de.tum.in.test.integration.testuser.subject.structural.subpackage; + +import de.tum.in.test.integration.testuser.subject.structural.AbstractClassExtension; + +public class SubpackageClass extends AbstractClassExtension { + // intentionally empty, used to test for inherited methods and attributes +}