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.TypedGenerator; 019import de.cuioss.test.generator.internal.net.java.quickcheck.generator.distribution.RandomConfiguration; 020import de.cuioss.test.generator.junit.GeneratorSeed; 021import org.junit.jupiter.api.extension.ExtensionContext; 022import org.junit.jupiter.params.provider.Arguments; 023import org.junit.jupiter.params.provider.ArgumentsProvider; 024import org.junit.platform.commons.JUnitException; 025import org.junit.platform.commons.util.ReflectionUtils; 026 027import java.lang.reflect.Method; 028import java.util.ArrayList; 029import java.util.Arrays; 030import java.util.List; 031import java.util.Optional; 032import java.util.stream.Stream; 033 034import static java.util.Objects.requireNonNull; 035 036/** 037 * Abstract base class for TypedGenerator-based ArgumentsProviders. 038 * Contains common functionality for seed management and generator handling. 039 * 040 * @author Oliver Wolff 041 * @since 2.0 042 */ 043public abstract class AbstractTypedGeneratorArgumentsProvider implements ArgumentsProvider { 044 045 /** 046 * Common error message part for method not found exceptions. 047 */ 048 protected static final String IN_CLASS = "] in class ["; 049 050 /** 051 * Provides arguments for the parameterized test. 052 * 053 * @param context the extension context 054 * @return a stream of arguments 055 * @throws Exception if an error occurs 056 */ 057 @Override 058 public Stream<? extends Arguments> provideArguments(ExtensionContext context) throws Exception { 059 // Handle seed management 060 var previousSeed = RandomConfiguration.getLastSeed(); 061 var useSeed = determineSeed(context); 062 063 if (useSeed != previousSeed) { 064 RandomConfiguration.setSeed(useSeed); 065 } 066 067 try { 068 return provideArgumentsForGenerators(context); 069 } finally { 070 // Restore previous seed if we changed it 071 if (useSeed != previousSeed) { 072 RandomConfiguration.setSeed(previousSeed); 073 } 074 } 075 } 076 077 /** 078 * Template method to be implemented by subclasses to provide arguments. 079 * 080 * @param context the extension context 081 * @return a stream of arguments 082 */ 083 @SuppressWarnings("java:S1452") // This wildcard is because of the TypedGenerator interface. Ok for testing 084 protected abstract Stream<? extends Arguments> provideArgumentsForGenerators(ExtensionContext context); 085 086 /** 087 * Gets the seed value specified for this provider. 088 * 089 * @return the seed value, or -1 if not specified 090 */ 091 protected abstract long getSeed(); 092 093 /** 094 * Gets the count of values to generate. 095 * 096 * @return the count 097 */ 098 protected abstract int getCount(); 099 100 /** 101 * Generates arguments from a TypedGenerator. 102 * 103 * @param generator the generator to use 104 * @return a list of Arguments 105 */ 106 protected List<Arguments> generateArguments(TypedGenerator<?> generator) { 107 List<Arguments> arguments = new ArrayList<>(); 108 for (int i = 0; i < getCount(); i++) { 109 arguments.add(Arguments.of(generator.next())); 110 } 111 return arguments; 112 } 113 114 /** 115 * Determines the seed to use for the generator based on the following priority: 116 * 1. Seed specified in the annotation (if not -1) 117 * 2. Seed from @GeneratorSeed on the test method 118 * 3. Seed from @GeneratorSeed on the test class 119 * 4. Current global seed from RandomConfiguration 120 * 121 * @param context the extension context 122 * @return the seed to use 123 */ 124 @SuppressWarnings("java:S3655") // False positive, isPresent() is checked 125 protected long determineSeed(ExtensionContext context) { 126 // If seed is explicitly set in the annotation, use it 127 long seed = getSeed(); 128 if (seed != -1L) { 129 return seed; 130 } 131 132 // Check for @GeneratorSeed on method or class 133 if (context.getElement().isPresent()) { 134 var element = context.getElement().get(); 135 136 // Check method first 137 if (element instanceof Method method) { 138 var seedAnnotation = method.getAnnotation(GeneratorSeed.class); 139 if (seedAnnotation != null) { 140 return seedAnnotation.value(); 141 } 142 143 // Then check class 144 var classAnnotation = method.getDeclaringClass().getAnnotation(GeneratorSeed.class); 145 if (classAnnotation != null) { 146 return classAnnotation.value(); 147 } 148 } 149 } 150 151 // Fall back to current global seed 152 return RandomConfiguration.getLastSeed(); 153 } 154 155 /** 156 * Creates an instance of the specified generator class using its no-args constructor. 157 * 158 * @param generatorClass the generator class to instantiate 159 * @return a new instance of the generator 160 * @throws JUnitException if the generator cannot be instantiated 161 */ 162 @SuppressWarnings("java:S1452") // This wildcard is because of the TypedGenerator interface. Ok for testing 163 protected TypedGenerator<?> createGeneratorInstance(Class<? extends TypedGenerator<?>> generatorClass) { 164 requireNonNull(generatorClass, "Generator class must not be null"); 165 166 try { 167 return ReflectionUtils.newInstance(generatorClass); 168 } catch (Exception e) { 169 throw new JUnitException( 170 "Failed to create TypedGenerator instance for " + generatorClass.getName() + 171 ". Make sure it has a public no-args constructor.", e); 172 } 173 } 174 175 /** 176 * Finds a method in the given class that returns a TypedGenerator and takes no parameters. 177 * 178 * @param clazz the class to search in 179 * @param methodName the method name to find 180 * @return an Optional containing the method, or empty if not found 181 */ 182 protected Optional<Method> findMethod(Class<?> clazz, String methodName) { 183 return Arrays.stream(clazz.getDeclaredMethods()) 184 .filter(m -> m.getName().equals(methodName)) 185 .filter(m -> m.getParameterCount() == 0) 186 .filter(m -> TypedGenerator.class.isAssignableFrom(m.getReturnType())) 187 .findFirst(); 188 } 189}