package org.ovirt.engine.core.bll; import static org.hamcrest.Matchers.empty; import static org.junit.Assert.assertThat; import java.lang.reflect.Constructor; import java.lang.reflect.Modifier; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; import org.hamcrest.BaseMatcher; import org.hamcrest.Description; import org.junit.BeforeClass; import org.junit.Test; import org.ovirt.engine.core.bll.context.CommandContext; import org.ovirt.engine.core.common.action.VdcActionParametersBase; import org.ovirt.engine.core.common.action.VdcActionType; import org.ovirt.engine.core.compat.Guid; import org.ovirt.engine.core.utils.ReflectionUtils; public class CommandCtorsTest { private static Collection<Class<CommandBase<? extends VdcActionParametersBase>>> commandClasses; private static Predicate<Constructor<?>> parametersAndContextConstructorSignature; private static Predicate<Constructor<?>> guidConstructorSignature; @BeforeClass public static void initCommandsCollection() { // Create a stream of all VdcActionType objects. commandClasses = Arrays.stream(VdcActionType.values()) // Filter out the Unknown VdcActionType. .filter(vdcActionType -> vdcActionType != VdcActionType.Unknown) // Map each vdcActionType to its appropriate command. .map(vdcActionType -> CommandsFactory.getCommandClass(vdcActionType.toString())) .collect(Collectors.toList()); } @BeforeClass public static void createConstructorsSignatures() { /** * Signature for a constructor that receives a Guid object. * This is a mandatory constructor in case that the class is annotated with * 'NonTransactiveCommandAttribute' and its 'forceCompensation' attribute * is set to true. */ guidConstructorSignature = createConstructorSignaturePredicate(Guid.class); /** * Signature for a constructor that receives parameters and context objects. * This is a mandatory constructor. */ parametersAndContextConstructorSignature = createConstructorSignaturePredicate(VdcActionParametersBase.class, CommandContext.class); } /** * {@link CommandsFactory} can call three different types of command's constructors * (see {@link #getConstructorRequiredByCommandsFactoryPredicate()} method for more details). * This test verifies that each of these constructors is accessible from {@link CommandsFactory}. * Since we create command objects using reflection, this test can be considered as a compilation * check for the accessibility of {@link CommandsFactory} to the commands' constructors. */ @Test public void testCommandsConstructorsContract() { Package commandsFactoryPackage = CommandsFactory.class.getPackage(); Predicate<Constructor<?>> constructorRequiredByCommandsFactory = getConstructorRequiredByCommandsFactoryPredicate(); Predicate<Constructor<?>> constructorInaccessibleFromPackagePredicate = createConstructorInaccessibleFromPackagePredicate(commandsFactoryPackage); Map<Class<?>, List<Constructor<?>>> commandsWithInaccessibleConstructor = commandClasses.stream() .map(command -> Arrays.stream(command.getDeclaredConstructors())) // Filter out all constructors that are not required by CommandsFactory. .map(constructorStream -> constructorStream.filter(constructorRequiredByCommandsFactory)) // Filter only the constructors that are not accessible from CommandsFactory. .map(constructorStream -> constructorStream.filter(constructorInaccessibleFromPackagePredicate)) // Flat Stream<Stream<Constructor<?>>> to Stream<Constructor<?>>. .flatMap(Function.identity()) .collect(Collectors.groupingBy(Constructor::getDeclaringClass)); assertThat("There are commands with at least one inaccessible constructor from CommandsFactory:", commandsWithInaccessibleConstructor.entrySet(), new BaseMatcher<Set<Map.Entry<Class<?>, List<Constructor<?>>>>>() { @SuppressWarnings("unchecked") @Override public boolean matches(Object o) { return ((Set<Map.Entry<Class<?>, List<Constructor<?>>>>) o).isEmpty(); } @Override public void describeTo(Description description) { description.appendText("All constructors should be accessible"); } @SuppressWarnings("unchecked") @Override public void describeMismatch(Object item, Description description) { description.appendText("Found inaccessible constructors:" + System.lineSeparator()); String startStr = String.format(":%n\t"); String separatorStr = String.format("%n\t"); String endStr = String.format("%n%n"); ((Set<Map.Entry<Class<?>, List<Constructor<?>>>>) item).forEach( commandsWithInaccessibleConstructors -> description.appendValueList( commandsWithInaccessibleConstructors.getKey().getSimpleName() + startStr, separatorStr, endStr, commandsWithInaccessibleConstructors.getValue()) ); } }); } /** * Returns a predicate that gets a constructor and returns true iff * it's one of the constructors that are required by {@link CommandsFactory}. */ private Predicate<Constructor<?>> getConstructorRequiredByCommandsFactoryPredicate() { return parametersAndContextConstructorSignature.or(guidConstructorSignature); } /** * A constructor is inaccessible from sourcePackage iff one of the next is true: * - It's also located in sourcePackage and its modifier is private. * - It's located outside of sourcePackage and its modifier is not public. */ private Predicate<Constructor<?>> createConstructorInaccessibleFromPackagePredicate(Package sourcePackage) { return constructor -> Modifier.isPrivate(constructor.getModifiers()) || (!sourcePackage.equals(constructor.getDeclaringClass().getPackage()) && !Modifier.isPublic(constructor.getModifiers())); } /** * Gets an array of classes and returns a predicate that given a constructor, returns * true iff its signature is compatible with the signature composed from the class array. */ private static Predicate<Constructor<?>> createConstructorSignaturePredicate(Class<?>... constructorParametersTypes) { return constructor -> ReflectionUtils.isCompatible(constructor.getParameterTypes(), constructorParametersTypes); } @Test public void testCommandMandatoryConstructorsExistence() { Predicate<Constructor<?>> classForcesCompensation = constructor -> constructor.getDeclaringClass().isAnnotationPresent(NonTransactiveCommandAttribute.class) && constructor.getDeclaringClass().getAnnotation(NonTransactiveCommandAttribute.class) .forceCompensation(); Predicate<Class<CommandBase<? extends VdcActionParametersBase>>> classLacksParamsAndContextCtor = getPredicateForNoCtorMatchesGivenPredicate(parametersAndContextConstructorSignature); Predicate<Class<CommandBase<? extends VdcActionParametersBase>>> classLacksGuidCtor = getPredicateForNoCtorMatchesGivenPredicate(classForcesCompensation.negate().or(guidConstructorSignature)); List<String> commandsWithoutMandatoryConstructor = commandClasses.stream() .filter(classLacksParamsAndContextCtor.or(classLacksGuidCtor)) .map(Class::getSimpleName) .sorted() .collect(Collectors.toList()); assertThat("There are commands that don't contain at least one of the mandatory constructors:" + System.lineSeparator() + "1. A constructor that receives parameters and context objects." + System.lineSeparator() + "2. A constructor that receives a Guid object, its class is annotated with '" + NonTransactiveCommandAttribute.class.getSimpleName() + "' and the annotation's 'forceCompensation' " + "attribute is set to true.", commandsWithoutMandatoryConstructor, empty()); } private Predicate<Class<CommandBase<? extends VdcActionParametersBase>>> getPredicateForNoCtorMatchesGivenPredicate( Predicate<Constructor<?>> constructorPredicate) { return commandClass -> Arrays.stream(commandClass.getDeclaredConstructors()).noneMatch(constructorPredicate); } }