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.generator.junit;
017
018import java.lang.reflect.Method;
019
020import org.junit.jupiter.api.extension.BeforeEachCallback;
021import org.junit.jupiter.api.extension.ExtendWith;
022import org.junit.jupiter.api.extension.Extension;
023import org.junit.jupiter.api.extension.ExtensionContext;
024import org.junit.jupiter.api.extension.TestExecutionExceptionHandler;
025import org.opentest4j.AssertionFailedError;
026import org.opentest4j.TestAbortedException;
027
028import de.cuioss.test.generator.internal.net.java.quickcheck.generator.distribution.RandomConfiguration;
029
030/**
031 * If enabled, either by using {@link ExtendWith} or
032 * {@link EnableGeneratorController} this {@link Extension} controls the seed
033 * initialization, by checking for {@link GeneratorSeed} and intercepts
034 * Test-failures by printing information providing the seed to reproduce.
035 *
036 * @author Oliver Wolff
037 *
038 */
039public class GeneratorControllerExtension implements BeforeEachCallback, TestExecutionExceptionHandler {
040
041    private static final String MSG_TEMPLATE = """
042            %s
043            GeneratorController seed was %sL.\s
044            Use a fixed seed by applying @GeneratorSeed(%sL) for the method/class,\s
045            or by using the system property '-D\
046            """ + RandomConfiguration.SEED_SYSTEM_PROPERTY + "=%s'\n";
047
048    @Override
049    public void handleTestExecutionException(ExtensionContext context, Throwable throwable) throws Throwable {
050
051        if (throwable instanceof TestAbortedException) {
052            throw throwable;
053        }
054        if (throwable instanceof AssertionFailedError) {
055            var failure = new AssertionFailedError(createErrorMessage(throwable, RandomConfiguration.getLastSeed()));
056            failure.setStackTrace(throwable.getStackTrace());
057            throw failure;
058        } else {
059            var failure = new AssertionFailedError(
060                    throwable.getClass() + ": " + createErrorMessage(throwable, RandomConfiguration.getLastSeed()));
061            failure.setStackTrace(throwable.getStackTrace());
062            throw failure;
063        }
064
065    }
066
067    @Override
068    @SuppressWarnings("java:S3655") // owolff: false positive: isPresent is checked
069    public void beforeEach(ExtensionContext context) throws Exception {
070        var seedSetByAnnotation = false;
071        long initialSeed = -1;
072        if (context.getElement().isPresent()) {
073            var annotatedElement = context.getElement().get();
074            var seedAnnotation = annotatedElement.getAnnotation(GeneratorSeed.class);
075            if (null == seedAnnotation && annotatedElement instanceof Method method) {
076                seedAnnotation = method.getDeclaringClass().getAnnotation(GeneratorSeed.class);
077            }
078            if (null != seedAnnotation) {
079                initialSeed = seedAnnotation.value();
080                seedSetByAnnotation = true;
081            }
082        }
083        if (seedSetByAnnotation) {
084            RandomConfiguration.setSeed(initialSeed);
085        } else {
086            RandomConfiguration.initSeed();
087        }
088    }
089
090    private String createErrorMessage(Throwable e, Long seed) {
091        var causeMsg = e.getMessage() == null ? "" : e.getMessage();
092        return MSG_TEMPLATE.formatted(causeMsg, seed, seed, seed);
093    }
094
095}