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; 017 018import de.cuioss.test.generator.internal.net.java.quickcheck.generator.distribution.RandomConfiguration; 019import org.junit.jupiter.api.extension.BeforeEachCallback; 020import org.junit.jupiter.api.extension.ExtensionContext; 021import org.junit.jupiter.api.extension.TestExecutionExceptionHandler; 022import org.opentest4j.AssertionFailedError; 023import org.opentest4j.TestAbortedException; 024 025import java.lang.reflect.Method; 026 027/** 028 * JUnit 5 extension that manages test data generation by controlling generator seeds 029 * and providing detailed failure information for test reproduction. 030 * 031 * <h2>Features</h2> 032 * <ul> 033 * <li>Initializes generator seeds before each test</li> 034 * <li>Supports seed configuration via {@link GeneratorSeed} annotation</li> 035 * <li>Provides detailed failure information for test reproduction</li> 036 * <li>Handles both method and class-level seed configuration</li> 037 * </ul> 038 * 039 * <h2>Usage</h2> 040 * Can be enabled in two ways: 041 * <pre> 042 * // Option 1: Direct extension usage 043 * @ExtendWith(GeneratorControllerExtension.class) 044 * class MyTest { 045 * @Test 046 * void shouldGenerateData() { ... } 047 * } 048 * 049 * // Option 2: Via meta-annotation (preferred) 050 * @EnableGeneratorController 051 * class MyTest { 052 * @Test 053 * void shouldGenerateData() { ... } 054 * } 055 * </pre> 056 * 057 * <h2>Seed Configuration</h2> 058 * Seeds can be configured in order of precedence: 059 * <ol> 060 * <li>Method-level {@code @GeneratorSeed}</li> 061 * <li>Class-level {@code @GeneratorSeed}</li> 062 * <li>System property {@code de.cuioss.test.generator.seed}</li> 063 * <li>Random seed (if no configuration present)</li> 064 * </ol> 065 * 066 * <h2>Failure Handling</h2> 067 * On test failure, provides a detailed message with: 068 * <ul> 069 * <li>The original test failure message</li> 070 * <li>The seed used for test data generation</li> 071 * <li>Instructions for test reproduction</li> 072 * </ul> 073 * 074 * @author Oliver Wolff 075 * @see EnableGeneratorController 076 * @see GeneratorSeed 077 * @see RandomConfiguration#SEED_SYSTEM_PROPERTY 078 */ 079public class GeneratorControllerExtension implements BeforeEachCallback, TestExecutionExceptionHandler { 080 081 private static final String MSG_TEMPLATE = """ 082 %s 083 GeneratorController seed was %sL.\s 084 Use a fixed seed by applying @GeneratorSeed(%sL) for the method/class,\s 085 or by using the system property '-D\ 086 """ + RandomConfiguration.SEED_SYSTEM_PROPERTY + "=%s'\n"; 087 088 @Override 089 public void handleTestExecutionException(ExtensionContext context, Throwable throwable) throws Throwable { 090 091 if (throwable instanceof TestAbortedException) { 092 throw throwable; 093 } 094 if (throwable instanceof AssertionFailedError) { 095 var failure = new AssertionFailedError(createErrorMessage(throwable, RandomConfiguration.getLastSeed())); 096 failure.setStackTrace(throwable.getStackTrace()); 097 throw failure; 098 } 099 var failure = new AssertionFailedError( 100 throwable.getClass() + ": " + createErrorMessage(throwable, RandomConfiguration.getLastSeed())); 101 failure.setStackTrace(throwable.getStackTrace()); 102 throw failure; 103 104 } 105 106 @Override 107 @SuppressWarnings("java:S3655") // owolff: false positive: isPresent is checked 108 public void beforeEach(ExtensionContext context) { 109 var seedSetByAnnotation = false; 110 long initialSeed = -1; 111 if (context.getElement().isPresent()) { 112 var annotatedElement = context.getElement().get(); 113 var seedAnnotation = annotatedElement.getAnnotation(GeneratorSeed.class); 114 if (null == seedAnnotation && annotatedElement instanceof Method method) { 115 seedAnnotation = method.getDeclaringClass().getAnnotation(GeneratorSeed.class); 116 } 117 if (null != seedAnnotation) { 118 initialSeed = seedAnnotation.value(); 119 seedSetByAnnotation = true; 120 } 121 } 122 if (seedSetByAnnotation) { 123 RandomConfiguration.setSeed(initialSeed); 124 } else { 125 RandomConfiguration.initSeed(); 126 } 127 } 128 129 private String createErrorMessage(Throwable e, Long seed) { 130 var causeMsg = e.getMessage() == null ? "" : e.getMessage(); 131 return MSG_TEMPLATE.formatted(causeMsg, seed, seed, seed); 132 } 133 134}