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}