001/*
002 * Copyright 2023 the original author or authors.
003 * <p>
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 * <p>
008 * https://www.apache.org/licenses/LICENSE-2.0
009 * <p>
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package de.cuioss.test.valueobjects.contract;
017
018import static de.cuioss.tools.collect.CollectionLiterals.immutableList;
019import static java.util.Objects.requireNonNull;
020import static org.junit.jupiter.api.Assertions.assertEquals;
021import static org.junit.jupiter.api.Assertions.assertFalse;
022
023import java.util.Arrays;
024import java.util.Collection;
025import java.util.HashSet;
026import java.util.List;
027import java.util.Optional;
028import java.util.Set;
029
030import de.cuioss.test.valueobjects.api.TestContract;
031import de.cuioss.test.valueobjects.api.contracts.VerifyConstructor;
032import de.cuioss.test.valueobjects.api.contracts.VerifyCopyConstructor;
033import de.cuioss.test.valueobjects.api.contracts.VerifyFactoryMethod;
034import de.cuioss.test.valueobjects.generator.impl.DummyGenerator;
035import de.cuioss.test.valueobjects.objects.ParameterizedInstantiator;
036import de.cuioss.test.valueobjects.objects.RuntimeProperties;
037import de.cuioss.test.valueobjects.objects.impl.ConstructorBasedInstantiator;
038import de.cuioss.test.valueobjects.property.PropertyMetadata;
039import de.cuioss.test.valueobjects.property.PropertySupport;
040import de.cuioss.test.valueobjects.property.impl.PropertyMetadataImpl;
041import de.cuioss.test.valueobjects.util.DeepCopyTestHelper;
042import de.cuioss.test.valueobjects.util.PropertyHelper;
043import de.cuioss.tools.logging.CuiLogger;
044import de.cuioss.tools.reflect.MoreReflection;
045import de.cuioss.tools.string.Joiner;
046import lombok.AccessLevel;
047import lombok.Getter;
048import lombok.NonNull;
049import lombok.RequiredArgsConstructor;
050
051/**
052 * TestContract for dealing Constructor and factories, {@link VerifyConstructor}
053 * and {@link VerifyFactoryMethod} respectively
054 *
055 * @author Oliver Wolff
056 * @param <T> identifying the of objects to be tested.
057 */
058@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
059public class CopyConstructorContractImpl<T> implements TestContract<T> {
060
061    private static final CuiLogger log = new CuiLogger(CopyConstructorContractImpl.class);
062
063    private static final String PROPERTY_NAME_COPY_FROM = "copyFrom";
064
065    /** This instantiator represents the Copy-Constructor. */
066    private final ParameterizedInstantiator<T> copyInstantiator;
067
068    /** represents the underlying Instantiator. */
069    @NonNull
070    @Getter
071    private final ParameterizedInstantiator<T> instantiator;
072
073    private final Set<String> consideredAttributes;
074    private final boolean useObjectEquals;
075
076    private final boolean verifyDeepCopy;
077    private final Collection<String> verifyDeepCopyIgnore;
078
079    @Override
080    public void assertContract() {
081        var builder = new StringBuilder("Verifying ");
082        builder.append(getClass().getName()).append("\nWith instantiator: ").append(copyInstantiator.toString())
083                .append("\nWith sourceInstantiator: ").append(instantiator.toString());
084        log.info(builder.toString());
085
086        final var sourceAttributeNames = RuntimeProperties
087                .extractNames(instantiator.getRuntimeProperties().getAllProperties());
088
089        final Set<String> compareAttributes = new HashSet<>(consideredAttributes);
090
091        if (!sourceAttributeNames.containsAll(consideredAttributes)) {
092            builder = new StringBuilder("Not all attributes can be checked at field level:");
093            builder.append("\nSource attributes are: ").append(sourceAttributeNames);
094            builder.append("\nCompare attributes are: ").append(consideredAttributes);
095            log.warn(builder.toString());
096            compareAttributes.retainAll(sourceAttributeNames);
097        }
098        log.info("Attributes being compared at field level are: " + Joiner.on(", ").join(compareAttributes));
099
100        assertCopyConstructor(compareAttributes);
101        assertDeepCopy();
102    }
103
104    private void assertDeepCopy() {
105        if (!verifyDeepCopy) {
106            log.debug("Not checking deep-copy, disabled by configuration");
107            return;
108        }
109        log.info("Verifying deep-copy, ignoring properties: {}" + verifyDeepCopyIgnore);
110
111        final var all = instantiator.getRuntimeProperties().getAllAsPropertySupport(true);
112
113        final var copyAttribute = copyInstantiator.getRuntimeProperties().getAllAsPropertySupport(false).iterator()
114                .next();
115        copyAttribute.setGeneratedValue(instantiator.newInstance(all, false));
116        var original = copyAttribute.getGeneratedValue();
117        Object copy = copyInstantiator.newInstance(immutableList(copyAttribute), false);
118        DeepCopyTestHelper.verifyDeepCopy(original, copy, verifyDeepCopyIgnore);
119    }
120
121    private void assertCopyConstructor(final Set<String> compareAttributes) {
122
123        final var all = instantiator.getRuntimeProperties().getAllAsPropertySupport(true);
124
125        final var copyAttribute = copyInstantiator.getRuntimeProperties().getAllAsPropertySupport(false).iterator()
126                .next();
127        copyAttribute.setGeneratedValue(instantiator.newInstance(all, false));
128
129        final var copy = copyInstantiator.newInstance(immutableList(copyAttribute), false);
130
131        final var fieldLevelCheck = all.stream().filter(p -> compareAttributes.contains(p.getName())).toList();
132
133        for (final PropertySupport field : fieldLevelCheck) {
134            if (field.isReadable()) {
135                field.assertValueSet(copy);
136            }
137        }
138        if (useObjectEquals) {
139            assertEquals(copyAttribute.getGeneratedValue(), copy);
140        }
141    }
142
143    /**
144     * Factory method for creating a an {@link CopyConstructorContractImpl}
145     * depending on the given parameter
146     *
147     * @param beanType                identifying the type to be tested. Must not be
148     *                                null
149     * @param annotated               the annotated unit-test-class. It is expected
150     *                                to be annotated with
151     *                                {@link VerifyCopyConstructor} method will
152     *                                return {@link Optional#empty()}
153     * @param initialPropertyMetadata identifying the complete set of
154     *                                {@link PropertyMetadata}, where the actual
155     *                                {@link PropertyMetadata} for the test will be
156     *                                filtered by using the attributes defined
157     *                                within {@link VerifyCopyConstructor}. Must not
158     *                                be null.
159     * @param existingContracts       identifying the already configured contracts.
160     *                                Must not be null nor empty.
161     * @return an {@link Optional} of {@link CopyConstructorContractImpl} in case
162     *         all requirements for the parameters are correct, otherwise it will
163     *         return {@link Optional#empty()}
164     */
165    public static final <T> Optional<CopyConstructorContractImpl<T>> createTestContract(final Class<T> beanType,
166            final Class<?> annotated, final List<PropertyMetadata> initialPropertyMetadata,
167            final List<TestContract<T>> existingContracts) {
168
169        requireNonNull(annotated, "annotated must not be null");
170
171        final Optional<VerifyCopyConstructor> configOption = MoreReflection.extractAnnotation(annotated,
172                VerifyCopyConstructor.class);
173
174        if (configOption.isEmpty()) {
175            return Optional.empty();
176        }
177        requireNonNull(beanType, "beantype must not be null");
178        requireNonNull(initialPropertyMetadata, "initialPropertyMetadata must not be null");
179        requireNonNull(existingContracts, "existingContracts must not be null");
180        assertFalse(existingContracts.isEmpty(), "There must be at least one VerifyContract defined");
181
182        final var config = configOption.get();
183
184        final var filtered = PropertyHelper.handleWhiteAndBlacklistAsList(config.of(), config.exclude(),
185                initialPropertyMetadata);
186        final var filteredNames = RuntimeProperties.extractNames(filtered);
187
188        final ParameterizedInstantiator<T> sourceInstantiator = findFittingInstantiator(existingContracts,
189                filteredNames);
190
191        final ParameterizedInstantiator<T> copyInstantiator = createCopyInstantiator(config, beanType);
192
193        return Optional.of(new CopyConstructorContractImpl<>(copyInstantiator, sourceInstantiator, filteredNames,
194                config.useObjectEquals(), config.verifyDeepCopy(), Arrays.asList(config.verifyDeepCopyIgnore())));
195    }
196
197    private static <T> ParameterizedInstantiator<T> createCopyInstantiator(final VerifyCopyConstructor config,
198            final Class<T> beanType) {
199        Class<?> target = beanType;
200        if (!VerifyCopyConstructor.class.equals(config.argumentType())) {
201            target = config.argumentType();
202        }
203        final var meta = PropertyMetadataImpl.builder().name(PROPERTY_NAME_COPY_FROM)
204                .generator(new DummyGenerator<>(target)).propertyClass(target).build();
205
206        return new ConstructorBasedInstantiator<>(beanType, new RuntimeProperties(immutableList(meta)));
207    }
208
209    static <T> ParameterizedInstantiator<T> findFittingInstantiator(final List<TestContract<T>> existingContracts,
210            final Set<String> filteredNames) {
211        for (final TestContract<T> contract : existingContracts) {
212            final var contractNames = RuntimeProperties
213                    .extractNames(contract.getInstantiator().getRuntimeProperties().getAllProperties());
214            if (contractNames.containsAll(filteredNames)) {
215                return contract.getInstantiator();
216            }
217        }
218        log.warn("No fitting ParameterizedInstantiator found, using best-fit");
219        return existingContracts.iterator().next().getInstantiator();
220    }
221
222}