main.fr.xgouchet.elmyr.junit5.ForgeExtension.kt Maven / Gradle / Ivy
package fr.xgouchet.elmyr.junit5
import fr.xgouchet.elmyr.Forge
import fr.xgouchet.elmyr.ForgeConfigurator
import fr.xgouchet.elmyr.annotation.Forgery
import fr.xgouchet.elmyr.inject.DefaultForgeryInjector
import fr.xgouchet.elmyr.inject.ForgeryInjector
import fr.xgouchet.elmyr.junit5.params.AdvancedForgeryParamResolver
import fr.xgouchet.elmyr.junit5.params.BooleanForgeryParamResolver
import fr.xgouchet.elmyr.junit5.params.DoubleForgeryParamResolver
import fr.xgouchet.elmyr.junit5.params.FloatForgeryParamResolver
import fr.xgouchet.elmyr.junit5.params.ForgeParamResolver
import fr.xgouchet.elmyr.junit5.params.ForgeryParamResolver
import fr.xgouchet.elmyr.junit5.params.IntForgeryParamResolver
import fr.xgouchet.elmyr.junit5.params.LongForgeryParamResolver
import fr.xgouchet.elmyr.junit5.params.MapForgeryParamResolver
import fr.xgouchet.elmyr.junit5.params.PairForgeryParamResolver
import fr.xgouchet.elmyr.junit5.params.RegexForgeryParamResolver
import fr.xgouchet.elmyr.junit5.params.StringForgeryParamResolver
import java.lang.reflect.Constructor
import java.lang.reflect.Type
import java.util.Locale
import org.junit.jupiter.api.extension.BeforeAllCallback
import org.junit.jupiter.api.extension.BeforeEachCallback
import org.junit.jupiter.api.extension.ExtensionContext
import org.junit.jupiter.api.extension.ParameterContext
import org.junit.jupiter.api.extension.ParameterResolver
import org.junit.jupiter.api.extension.TestExecutionExceptionHandler
import org.junit.platform.commons.support.AnnotationSupport
/**
* A JUnit Jupiter extension that can inject forgeries in the test class's fields/properties/method
* parameters (when annotated with [Forgery]). It can also inject a [Forge] instance in test methods
* without the need to annotate it.
*
* @see Forgery
* @see Forge
*/
class ForgeExtension :
BeforeAllCallback,
BeforeEachCallback,
TestExecutionExceptionHandler,
ParameterResolver,
ForgeryInjector.Listener {
internal val instanceForge: Forge = Forge()
private val injector: ForgeryInjector = DefaultForgeryInjector()
private val injectedData: MutableList> = mutableListOf()
private val parameterResolvers = listOf(
ForgeParamResolver,
BooleanForgeryParamResolver,
IntForgeryParamResolver,
LongForgeryParamResolver,
FloatForgeryParamResolver,
DoubleForgeryParamResolver,
StringForgeryParamResolver,
RegexForgeryParamResolver,
ForgeryParamResolver,
AdvancedForgeryParamResolver,
MapForgeryParamResolver,
PairForgeryParamResolver
)
// region BeforeAllCallback
/** @inheritdoc */
override fun beforeAll(context: ExtensionContext) {
val configurators = getConfigurators(context)
configurators.forEach {
it.configure(instanceForge)
}
val globalStore = context.getStore(ExtensionContext.Namespace.GLOBAL)
globalStore.put(EXTENSION_STORE_FORGE_KEY, instanceForge)
}
// endregion
// region BeforeEachCallback
/** @inheritdoc */
override fun beforeEach(context: ExtensionContext) {
resetSeed(context)
injectedData.clear()
val target = context.requiredTestInstance
injector.inject(instanceForge, target, this)
}
// endregion
// region TestExecutionExceptionHandler
/** @inheritdoc */
override fun handleTestExecutionException(context: ExtensionContext, throwable: Throwable) {
val configuration = getConfigurations(context).firstOrNull()
val errorMessage = "<%s.%s()> failed with Forge seed 0x%xL".format(
Locale.US,
context.requiredTestInstance.javaClass.simpleName,
context.requiredTestMethod.name,
instanceForge.seed
)
val injectedMessage = if (injectedData.isEmpty()) "" else {
injectedData.joinToString(
separator = "\n",
prefix = " and:\n",
postfix = "\n"
) { "\t- ${it.type} ${it.parent}::${it.name} = ${it.value}" }
}
val helpMessage = if (configuration == null) {
"\nAdd the following @ForgeConfiguration annotation to your test class :\n\n" +
"\t@ForgeConfiguration(seed = 0x%xL)\n".format(
Locale.US,
instanceForge.seed
)
} else {
"\nAdd the seed in your @ForgeConfiguration annotation :\n\n" +
"\t@ForgeConfiguration(value = %s::class, seed = 0x%xL)\n".format(
Locale.US,
configuration.value.simpleName,
instanceForge.seed
)
}
System.err.println(
errorMessage + injectedMessage + helpMessage
)
throw throwable
}
// endregion
// region ParameterResolver
/** @inheritdoc */
override fun supportsParameter(
parameterContext: ParameterContext,
extensionContext: ExtensionContext
): Boolean {
val isSupported = parameterResolvers.any {
it.supportsParameter(parameterContext, extensionContext)
}
if (isSupported && parameterContext.declaringExecutable is Constructor<*>) {
throw IllegalStateException(
"@Forgery is not supported on constructor parameters. " +
"Please use field injection instead."
)
}
return isSupported
}
/** @inheritdoc */
override fun resolveParameter(
parameterContext: ParameterContext,
extensionContext: ExtensionContext
): Any? {
val resolver = parameterResolvers.firstOrNull {
it.supportsParameter(parameterContext, extensionContext)
}
val value = resolver?.resolveParameter(parameterContext, extensionContext, instanceForge)
val target = ForgeTarget.ForgeParamTarget(
parameterContext.parameter.declaringExecutable.name,
parameterContext.parameter.name,
value
)
injectedData.add(target)
return value
}
// endregion
// region ForgeryInjector.Listener
/** @inheritdoc */
override fun onFieldInjected(
declaringClass: Class<*>,
fieldType: Type,
fieldName: String,
value: Any?
) {
injectedData.add(
ForgeTarget.ForgeFieldTarget(
declaringClass.simpleName,
fieldName,
value
)
)
}
// endregion
// region Internal
private fun resetSeed(context: ExtensionContext) {
val configurations = getConfigurations(context)
val seed = configurations.map { it.seed }
.firstOrNull { it != 0L }
instanceForge.seed = seed ?: Forge.seed()
}
private fun getConfigurations(context: ExtensionContext): List {
val result = mutableListOf()
var currentContext = context
while (currentContext != context.root) {
val annotation = AnnotationSupport
.findAnnotation(
currentContext.element,
ForgeConfiguration::class.java
)
if (annotation.isPresent) {
result.add(annotation.get())
}
if (currentContext.parent.isPresent) {
currentContext = currentContext.parent.get()
} else {
break
}
}
return result
}
private fun getConfigurators(context: ExtensionContext): List {
return getConfigurations(context)
.map {
if (it.value.java == ForgeConfigurator.NoOp.javaClass) {
ForgeConfigurator.NoOp
} else {
val constructor = it.value.java.constructors.firstOrNull {
it.parameterCount == 0
}
constructor?.newInstance() as? ForgeConfigurator ?: ForgeConfigurator.NoOp
}
}
}
// endregion
companion object {
/**
* The key used to store the [Forge] in an [ExtensionContext]'s global store.
*/
@JvmField
val EXTENSION_STORE_FORGE_KEY = "${ForgeExtension::class.java.canonicalName}.forge"
/**
* Retrieves the [Forge] stored in the given [ExtensionContext] global store.
* @param context the current [ExtensionContext].
* @return the valid forge for that context or null
*/
@JvmStatic
fun getForge(context: ExtensionContext): Forge? {
val globalStore = context.getStore(ExtensionContext.Namespace.GLOBAL)
val storedForge = globalStore.get(EXTENSION_STORE_FORGE_KEY)
return storedForge as? Forge
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy