001/* 002 * Copyright © 2025 CUI-OpenSource-Software (info@cuioss.de) 003 * 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 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 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.generator.junit.parameterized; 017 018import de.cuioss.test.generator.Generators; 019import de.cuioss.test.generator.TypedGenerator; 020import org.junit.jupiter.api.extension.ExtensionContext; 021import org.junit.jupiter.params.provider.Arguments; 022import org.junit.jupiter.params.support.AnnotationConsumer; 023import org.junit.platform.commons.JUnitException; 024 025import java.util.ArrayList; 026import java.util.List; 027import java.util.stream.Stream; 028 029/** 030 * Implementation of {@link org.junit.jupiter.params.provider.ArgumentsProvider} that provides arguments from multiple 031 * {@link TypedGenerator} instances for parameterized tests annotated with 032 * {@link CompositeTypeGeneratorSource}. 033 * 034 * <p> 035 * This provider creates multiple {@link TypedGenerator} instances from the specified 036 * classes and/or methods, and generates combinations of values to be used as test arguments. 037 * </p> 038 * 039 * <p> 040 * Seed management is integrated with the existing 041 * {@link de.cuioss.test.generator.junit.GeneratorControllerExtension} to ensure 042 * consistent and reproducible test data generation. 043 * </p> 044 * 045 * @author Oliver Wolff 046 * @since 2.0 047 * @see CompositeTypeGeneratorSource 048 * @see TypedGenerator 049 */ 050public class CompositeTypeGeneratorArgumentsProvider extends AbstractTypedGeneratorArgumentsProvider 051 implements AnnotationConsumer<CompositeTypeGeneratorSource> { 052 053 private Class<? extends TypedGenerator<?>>[] generatorClasses; 054 private String[] generatorMethods; 055 private GeneratorType[] generators; 056 private int count; 057 private boolean cartesianProduct; 058 059 @Override 060 public void accept(CompositeTypeGeneratorSource annotation) { 061 generatorClasses = annotation.generatorClasses(); 062 generatorMethods = annotation.generatorMethods(); 063 generators = annotation.generators(); 064 count = Math.max(1, annotation.count()); 065 cartesianProduct = annotation.cartesianProduct(); 066 } 067 068 @Override 069 protected Stream<? extends Arguments> provideArgumentsForGenerators(ExtensionContext context) { 070 if (generatorClasses.length == 0 && generatorMethods.length == 0 && generators.length == 0) { 071 throw new JUnitException("At least one generator class, method, or type must be specified"); 072 } 073 074 // Create generator instances 075 List<TypedGenerator<?>> generatorInstances = new ArrayList<>(); 076 077 // Add generators from classes 078 for (Class<? extends TypedGenerator<?>> generatorClass : generatorClasses) { 079 generatorInstances.add(createGeneratorInstance(generatorClass)); 080 } 081 082 // Add generators from methods 083 for (String methodName : generatorMethods) { 084 generatorInstances.add(GeneratorMethodResolver.getGenerator(methodName, context)); 085 } 086 087 // Add generators from GeneratorType enum values 088 for (GeneratorType generatorType : generators) { 089 generatorInstances.add(createGeneratorFromType(generatorType)); 090 } 091 092 // Generate values from each generator 093 List<List<Object>> generatedValues = new ArrayList<>(); 094 for (TypedGenerator<?> generator : generatorInstances) { 095 List<Object> values = new ArrayList<>(); 096 for (int i = 0; i < count; i++) { 097 values.add(generator.next()); 098 } 099 generatedValues.add(values); 100 } 101 102 // Create combinations of values 103 return createArgumentsCombinations(generatedValues); 104 } 105 106 @Override 107 protected long getSeed() { 108 return -1L; 109 } 110 111 @Override 112 protected int getCount() { 113 return count; 114 } 115 116 /** 117 * Creates combinations of values from the generated values. 118 * 119 * @param generatedValues lists of values from each generator 120 * @return a stream of Arguments with combinations of values 121 */ 122 private Stream<Arguments> createArgumentsCombinations(List<List<Object>> generatedValues) { 123 if (generatedValues.isEmpty()) { 124 return Stream.empty(); 125 } 126 127 if (cartesianProduct) { 128 return createCartesianProduct(generatedValues); 129 } else { 130 return createOneToOnePairs(generatedValues); 131 } 132 } 133 134 /** 135 * Creates a cartesian product of all generated values. 136 * 137 * @param generatedValues lists of values from each generator 138 * @return a stream of Arguments with all possible combinations 139 */ 140 private Stream<Arguments> createCartesianProduct(List<List<Object>> generatedValues) { 141 // Start with a single empty list 142 List<List<Object>> combinations = new ArrayList<>(); 143 combinations.add(new ArrayList<>()); 144 145 // For each list of generated values 146 for (List<Object> values : generatedValues) { 147 List<List<Object>> newCombinations = new ArrayList<>(); 148 149 // For each existing combination 150 for (List<Object> combination : combinations) { 151 // For each value in the current list 152 for (Object value : values) { 153 // Create a new combination by adding the value to the existing combination 154 List<Object> newCombination = new ArrayList<>(combination); 155 newCombination.add(value); 156 newCombinations.add(newCombination); 157 } 158 } 159 160 combinations = newCombinations; 161 } 162 163 // Convert combinations to Arguments 164 return combinations.stream() 165 .map(combination -> Arguments.of(combination.toArray())); 166 } 167 168 /** 169 * Creates one-to-one pairs of generated values. 170 * Requires all generators to produce the same number of values. 171 * 172 * @param generatedValues lists of values from each generator 173 * @return a stream of Arguments with one-to-one pairs 174 * @throws JUnitException if the lists have different sizes 175 */ 176 private Stream<Arguments> createOneToOnePairs(List<List<Object>> generatedValues) { 177 // Check that all lists have the same size 178 int size = generatedValues.getFirst().size(); 179 for (List<Object> values : generatedValues) { 180 if (values.size() != size) { 181 throw new JUnitException( 182 "When cartesianProduct is false, all generators must produce the same number of values"); 183 } 184 } 185 186 // Create one-to-one pairs 187 List<Arguments> arguments = new ArrayList<>(); 188 for (int i = 0; i < size; i++) { 189 Object[] args = new Object[generatedValues.size()]; 190 for (int j = 0; j < generatedValues.size(); j++) { 191 args[j] = generatedValues.get(j).get(i); 192 } 193 arguments.add(Arguments.of(args)); 194 } 195 196 return arguments.stream(); 197 } 198 199 /** 200 * Creates a TypedGenerator from a GeneratorType enum value. 201 * 202 * @param generatorType the generator type 203 * @return a TypedGenerator instance 204 * @throws JUnitException if the generator cannot be created 205 */ 206 @SuppressWarnings("java:S1452") // This wildcard is because of the TypedGenerator interface. Ok for testing 207 private TypedGenerator<?> createGeneratorFromType(GeneratorType generatorType) { 208 try { 209 // Use reflection to invoke the method on Generators class 210 var methodName = generatorType.getMethodName(); 211 var method = Generators.class.getMethod(methodName); 212 return (TypedGenerator<?>) method.invoke(null); 213 } catch (Exception e) { 214 throw new JUnitException("Failed to create generator from type: " + generatorType, e); 215 } 216 } 217}