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.lang.reflect.Constructor; 026import java.lang.reflect.InvocationTargetException; 027import java.lang.reflect.Method; 028import java.util.stream.Stream; 029 030import static java.util.Objects.requireNonNull; 031 032/** 033 * Implementation of {@link org.junit.jupiter.params.provider.ArgumentsProvider} that provides arguments from a 034 * {@link TypedGenerator} created by the {@link Generators} utility class for parameterized tests 035 * annotated with {@link GeneratorsSource}. 036 * 037 * <p> 038 * This provider invokes the specified generator method from the {@link Generators} class to obtain a {@link TypedGenerator} 039 * instance and generates the requested number of values to be used as test arguments. 040 * </p> 041 * 042 * <p> 043 * Seed management is integrated with the existing 044 * {@link de.cuioss.test.generator.junit.GeneratorControllerExtension} to ensure 045 * consistent and reproducible test data generation. 046 * </p> 047 * 048 * @author Oliver Wolff 049 * @see GeneratorsSource 050 * @see TypedGenerator 051 * @see Generators 052 * @since 2.0 053 */ 054public class GeneratorsSourceArgumentsProvider extends AbstractTypedGeneratorArgumentsProvider 055 implements AnnotationConsumer<GeneratorsSource> { 056 057 private GeneratorType generatorType; 058 private int minSize; 059 private int maxSize; 060 private String low; 061 private String high; 062 private int count; 063 064 @Override 065 public void accept(GeneratorsSource annotation) { 066 generatorType = annotation.generator(); 067 068 minSize = annotation.minSize(); 069 maxSize = annotation.maxSize(); 070 low = annotation.low(); 071 high = annotation.high(); 072 count = Math.max(1, annotation.count()); 073 } 074 075 @Override 076 protected Stream<? extends Arguments> provideArgumentsForGenerators(ExtensionContext context) { 077 // Create generator instance using factory 078 TypedGenerator<?> generator; 079 try { 080 generator = createGeneratorFromFactory(); 081 } catch (InvocationTargetException | NoSuchMethodException | InstantiationException | 082 IllegalAccessException e) { 083 throw new IllegalStateException(e); 084 } 085 086 // Generate values 087 return generateArguments(generator).stream(); 088 } 089 090 @Override 091 protected long getSeed() { 092 return -1L; 093 } 094 095 @Override 096 protected int getCount() { 097 return count; 098 } 099 100 /** 101 * Creates a TypedGenerator instance based on the generator type. 102 * 103 * @return a TypedGenerator instance 104 * @throws JUnitException if the generator cannot be created 105 */ 106 private TypedGenerator<?> createGeneratorFromFactory() throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { 107 requireNonNull(generatorType, "Generator type must not be null"); 108 109 // Check if this is a domain-specific generator (factory class is the generator class itself) 110 if (isDomainSpecificGenerator()) { 111 return createDomainSpecificGenerator(); 112 } 113 114 // Otherwise, it's a standard generator from the Generators class 115 String methodName = generatorType.getMethodName(); 116 requireNonNull(methodName, "Generator method name must not be null"); 117 118 return switch (generatorType.getParameterType()) { 119 case NEEDS_BOUNDS -> createStringGenerator(); 120 case NEEDS_RANGE -> createNumberGenerator(); 121 case PARAMETERLESS -> createParameterlessGenerator(); 122 }; 123 } 124 125 /** 126 * Checks if the generator type is a domain-specific generator. 127 * 128 * @return true if it's a domain-specific generator 129 */ 130 private boolean isDomainSpecificGenerator() { 131 return generatorType.getMethodName() == null && 132 generatorType.getFactoryClass() != null && 133 TypedGenerator.class.isAssignableFrom(generatorType.getFactoryClass()); 134 } 135 136 /** 137 * Creates a domain-specific generator by instantiating the generator class. 138 * 139 * @return a TypedGenerator instance 140 */ 141 private TypedGenerator<?> createDomainSpecificGenerator() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { 142 Class<?> generatorClass = generatorType.getFactoryClass(); 143 Constructor<?> constructor = generatorClass.getDeclaredConstructor(); 144 return (TypedGenerator<?>) constructor.newInstance(); 145 } 146 147 /** 148 * Creates a string-based generator using the minSize and maxSize parameters. 149 * 150 * @return a TypedGenerator for strings 151 */ 152 private TypedGenerator<?> createStringGenerator() throws InvocationTargetException, IllegalAccessException { 153 Method method = findMethodWithParameters(generatorType.getMethodName(), int.class, int.class); 154 return (TypedGenerator<?>) method.invoke(null, minSize, maxSize); 155 } 156 157 /** 158 * Creates a number-based generator using the low and high parameters. 159 * 160 * @return a TypedGenerator for numbers 161 */ 162 private TypedGenerator<?> createNumberGenerator() throws InvocationTargetException, IllegalAccessException { 163 // Determine the parameter types based on the generator return type 164 Class<?>[] paramTypes; 165 Object[] params; 166 167 Class<?> returnType = generatorType.getReturnType(); 168 169 if (Integer.class.equals(returnType)) { 170 paramTypes = new Class<?>[]{int.class, int.class}; 171 params = new Object[]{Integer.parseInt(low), Integer.parseInt(high)}; 172 } else if (Long.class.equals(returnType)) { 173 paramTypes = new Class<?>[]{long.class, long.class}; 174 params = new Object[]{Long.parseLong(low), Long.parseLong(high)}; 175 } else if (Double.class.equals(returnType)) { 176 paramTypes = new Class<?>[]{double.class, double.class}; 177 params = new Object[]{Double.parseDouble(low), Double.parseDouble(high)}; 178 } else if (Float.class.equals(returnType)) { 179 paramTypes = new Class<?>[]{float.class, float.class}; 180 params = new Object[]{Float.parseFloat(low), Float.parseFloat(high)}; 181 } else if (Short.class.equals(returnType)) { 182 paramTypes = new Class<?>[]{short.class, short.class}; 183 params = new Object[]{Short.parseShort(low), Short.parseShort(high)}; 184 } else if (Byte.class.equals(returnType)) { 185 paramTypes = new Class<?>[]{byte.class, byte.class}; 186 params = new Object[]{Byte.parseByte(low), Byte.parseByte(high)}; 187 } else { 188 throw new UnsupportedOperationException( 189 "Number generator for type '" + returnType.getSimpleName() + "' is not supported."); 190 } 191 192 Method method = findMethodWithParameters(generatorType.getMethodName(), paramTypes); 193 return (TypedGenerator<?>) method.invoke(null, params); 194 } 195 196 /** 197 * Creates a generator that doesn't require parameters. 198 * 199 * @return a TypedGenerator 200 */ 201 private TypedGenerator<?> createParameterlessGenerator() throws InvocationTargetException, IllegalAccessException { 202 Method method = findMethodWithParameters(generatorType.getMethodName()); 203 return (TypedGenerator<?>) method.invoke(null); 204 } 205 206 207 /** 208 * Finds a generator method in the Generators class with the specified parameter types. 209 * 210 * @param methodName the name of the method to find 211 * @param parameterTypes the parameter types the method should accept 212 * @return the generator method 213 * @throws JUnitException if the method cannot be found 214 */ 215 private Method findMethodWithParameters(String methodName, Class<?>... parameterTypes) { 216 try { 217 return Generators.class.getMethod(methodName, parameterTypes); 218 } catch (NoSuchMethodException e) { 219 throw new JUnitException( 220 "Could not find static generator method '" + methodName + 221 "' in Generators class with the specified parameter types", e); 222 } 223 } 224}