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}