Add @DefinitionSource annotations

Allows parameterized tests to use Item, Npc, and Object definitions
as @ArgumentSources.

This commit also adds support for using @ItemDefinitions etc on
properties as well as functions.
This commit is contained in:
Major-
2018-08-24 23:06:36 +01:00
parent dc0690f82d
commit fd52ee6026
6 changed files with 207 additions and 38 deletions
+6
View File
@@ -19,4 +19,10 @@ dependencies {
api group: 'com.willowtreeapps.assertk', name: 'assertk', version: assertkVersion
implementation group: 'org.powermock', name: 'powermock-module-junit4', version: powermockVersion
}
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
kotlinOptions {
jvmTarget = "1.8"
}
}
@@ -13,23 +13,14 @@ import org.apollo.game.model.entity.Npc
import org.apollo.game.model.entity.Player
import org.apollo.game.model.entity.obj.GameObject
import org.apollo.game.plugin.KotlinPluginEnvironment
import org.apollo.game.plugin.PluginMetaData
import org.apollo.game.plugin.testing.fakes.FakePluginContextFactory
import org.apollo.game.plugin.testing.junit.api.ActionCapture
import org.apollo.game.plugin.testing.junit.api.annotations.ItemDefinitions
import org.apollo.game.plugin.testing.junit.api.annotations.NpcDefinitions
import org.apollo.game.plugin.testing.junit.api.annotations.ObjectDefinitions
import org.apollo.game.plugin.testing.junit.mocking.StubPrototype
import org.junit.jupiter.api.extension.AfterAllCallback
import org.junit.jupiter.api.extension.AfterEachCallback
import org.junit.jupiter.api.extension.AfterTestExecutionCallback
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 java.util.ArrayList
import kotlin.reflect.KFunction
import org.junit.jupiter.api.extension.*
import kotlin.reflect.KCallable
import kotlin.reflect.KMutableProperty
import kotlin.reflect.full.companionObject
import kotlin.reflect.full.createType
@@ -58,7 +49,7 @@ class ApolloTestingExtension :
private fun cleanup(context: ExtensionContext) {
val store = context.getStore(namespace)
val state = store.get(ApolloTestState::class) as ApolloTestState
val state = store[ApolloTestState::class] as ApolloTestState
try {
state.actionCapture?.runAction()
@@ -84,27 +75,32 @@ class ApolloTestingExtension :
.map { it.kotlin.companionObject }
.ifPresent { companion ->
val companionInstance = companion.objectInstance!!
val testClassMethods = companion.declaredMemberFunctions
val callables: List<KCallable<*>> = companion.declaredMemberFunctions + companion.declaredMemberProperties
createTestDefinitions<ItemDefinition, ItemDefinitions>(
testClassMethods, companionInstance, ItemDefinition::getId, ItemDefinition::lookup
callables, companionInstance, ItemDefinition::getId, ItemDefinition::lookup,
ItemDefinition::getDefinitions
)
createTestDefinitions<NpcDefinition, NpcDefinitions>(
testClassMethods, companionInstance, NpcDefinition::getId, NpcDefinition::lookup
callables, companionInstance, NpcDefinition::getId, NpcDefinition::lookup,
NpcDefinition::getDefinitions
)
createTestDefinitions<ObjectDefinition, ObjectDefinitions>(
testClassMethods, companionInstance, ObjectDefinition::getId, ObjectDefinition::lookup
callables, companionInstance, ObjectDefinition::getId, ObjectDefinition::lookup,
ObjectDefinition::getDefinitions
)
}
val pluginEnvironment = KotlinPluginEnvironment(stubWorld)
pluginEnvironment.setContext(FakePluginContextFactory.create(stubHandlers))
pluginEnvironment.load(ArrayList<PluginMetaData>())
KotlinPluginEnvironment(stubWorld).apply {
setContext(FakePluginContextFactory.create(stubHandlers))
load(emptyList())
}
val state = ApolloTestState(stubHandlers, stubWorld)
val store = context.getStore(namespace)
val state = ApolloTestState(stubHandlers, stubWorld)
store.put(ApolloTestState::class, state)
}
@@ -151,30 +147,43 @@ class ApolloTestingExtension :
* @param lookup The lookup function that returns an instance of [D] given a definition id.
*/
private inline fun <reified D : Any, reified A : Annotation> createTestDefinitions(
testClassMethods: Collection<KFunction<*>>,
testClassMethods: Collection<KCallable<*>>,
companionObjectInstance: Any?,
crossinline idMapper: (D) -> Int,
crossinline lookup: (Int) -> D
crossinline lookup: (Int) -> D?,
crossinline getAll: () -> Array<D>
) {
val testDefinitions = testClassMethods.asSequence()
.filter { method -> method.annotations.any { it is A } }
.flatMap { method ->
@Suppress("UNCHECKED_CAST")
method as? KFunction<Collection<D>> ?: throw RuntimeException("${method.name} is annotated with " +
"${A::class.simpleName} but does not return Collection<${D::class.simpleName}."
)
method.isAccessible = true // lets us call methods in private companion objects
method.call(companionObjectInstance).asSequence()
}
val testDefinitions = findTestDefinitions<D, A>(testClassMethods, companionObjectInstance)
.associateBy(idMapper)
if (testDefinitions.isNotEmpty()) {
val idSlot = slot<Int>()
staticMockk<D>().mock()
every { lookup(capture(idSlot)) } answers { testDefinitions[idSlot.captured]!! }
every { lookup(capture(idSlot)) } answers { testDefinitions[idSlot.captured] }
every { getAll() } answers { testDefinitions.values.sortedBy(idMapper).toTypedArray() }
}
}
companion object {
inline fun <reified D : Any, reified A : Annotation> findTestDefinitions(
callables: Collection<KCallable<*>>,
companionObjectInstance: Any?
): List<D> {
return callables
.filter { method -> method.annotations.any { it is A } }
.flatMap { method ->
@Suppress("UNCHECKED_CAST")
method as? KCallable<Collection<D>> ?: throw RuntimeException("${method.name} is annotated with " +
"${A::class.simpleName} but does not return Collection<${D::class.simpleName}>."
)
method.isAccessible = true // lets us call methods in private companion objects
method.call(companionObjectInstance)
}
}
}
}
@@ -8,7 +8,7 @@ package org.apollo.game.plugin.testing.junit.api.annotations
* - Be inside a **companion object** inside an apollo test class (a regular object will not work).
* - Return a `Collection<ItemDefinition>`.
*/
@Target(AnnotationTarget.FUNCTION)
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)
annotation class ItemDefinitions
@@ -20,7 +20,7 @@ annotation class ItemDefinitions
* - Be inside a **companion object** inside an apollo test class (a regular object will not work).
* - Return a `Collection<NpcDefinition>`.
*/
@Target(AnnotationTarget.FUNCTION)
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)
annotation class NpcDefinitions
@@ -32,6 +32,6 @@ annotation class NpcDefinitions
* - Be inside a **companion object** inside an apollo test class (a regular object will not work).
* - Return a `Collection<ObjectDefinition>`.
*/
@Target(AnnotationTarget.FUNCTION)
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)
annotation class ObjectDefinitions
@@ -0,0 +1,100 @@
package org.apollo.game.plugin.testing.junit.params
import org.apollo.cache.def.ItemDefinition
import org.apollo.cache.def.NpcDefinition
import org.apollo.cache.def.ObjectDefinition
import org.apollo.game.plugin.testing.junit.ApolloTestingExtension.Companion.findTestDefinitions
import org.apollo.game.plugin.testing.junit.api.annotations.ItemDefinitions
import org.apollo.game.plugin.testing.junit.api.annotations.NpcDefinitions
import org.apollo.game.plugin.testing.junit.api.annotations.ObjectDefinitions
import org.junit.jupiter.api.extension.ExtensionContext
import org.junit.jupiter.params.provider.Arguments
import org.junit.jupiter.params.provider.ArgumentsProvider
import org.junit.jupiter.params.support.AnnotationConsumer
import java.util.stream.Stream
import kotlin.reflect.KCallable
import kotlin.reflect.full.companionObject
import kotlin.reflect.full.declaredMemberFunctions
import kotlin.reflect.full.declaredMemberProperties
/**
* An [ArgumentsProvider] for a definition of type `D`.
*/
abstract class DefinitionsProvider<D : Any>(
private val definitionProvider: (methods: Collection<KCallable<*>>, companionObjectInstance: Any) -> List<D>
) : ArgumentsProvider {
protected lateinit var sourceNames: Set<String>
override fun provideArguments(context: ExtensionContext): Stream<out Arguments> {
val companion = context.requiredTestClass.kotlin.companionObject
?: throw RuntimeException("${context.requiredTestMethod.name} is annotated with a DefinitionsProvider," +
" but does not contain a companion object to search for Definitions in."
)
val companionInstance = companion.objectInstance!! // safe
val callables: List<KCallable<*>> = companion.declaredMemberFunctions + companion.declaredMemberProperties
val filtered = if (sourceNames.isEmpty()) {
callables
} else {
callables.filter { it.name in sourceNames }
}
return definitionProvider(filtered, companionInstance).map { Arguments.of(it) }.stream()
}
}
// These providers are separate because of a JUnit bug in its use of ArgumentsSource and AnnotationConsumer -
// the reflection code that invokes the AnnotationConsumer searches for an accept() method that takes an
// Annotation parameter, prohibiting usage of the actual `Annotation` type as the parameter - meaning
// DefinitionsProvider cannot abstract over different annotation implementations (i.e. over ItemDefinitionSource,
// NpcDefinitionSource, and ObjectDefinitionSource).
/**
* An [ArgumentsProvider] for [ItemDefinition]s.
*
* Test authors should not need to utilise this class, and should instead annotate their function with
* [@ItemDefinitionSource][ItemDefinitionSource].
*/
object ItemDefinitionsProvider : DefinitionsProvider<ItemDefinition>(
{ methods, companion -> findTestDefinitions<ItemDefinition, ItemDefinitions>(methods, companion) }
), AnnotationConsumer<ItemDefinitionSource> {
override fun accept(source: ItemDefinitionSource) {
sourceNames = source.sourceNames.toHashSet()
}
}
/**
* An [ArgumentsProvider] for [NpcDefinition]s.
*
* Test authors should not need to utilise this class, and should instead annotate their function with
* [@NpcDefinitionSource][NpcDefinitionSource].
*/
object NpcDefinitionsProvider : DefinitionsProvider<NpcDefinition>(
{ methods, companion -> findTestDefinitions<NpcDefinition, NpcDefinitions>(methods, companion) }
), AnnotationConsumer<NpcDefinitionSource> {
override fun accept(source: NpcDefinitionSource) {
sourceNames = source.sourceNames.toHashSet()
}
}
/**
* An [ArgumentsProvider] for [ObjectDefinition]s.
*
* Test authors should not need to utilise this class, and should instead annotate their function with
* [@ObjectDefinitionSource][ObjectDefinitionSource].
*/
object ObjectDefinitionsProvider : DefinitionsProvider<ObjectDefinition>(
{ methods, companion -> findTestDefinitions<ObjectDefinition, ObjectDefinitions>(methods, companion) }
), AnnotationConsumer<ObjectDefinitionSource> {
override fun accept(source: ObjectDefinitionSource) {
sourceNames = source.sourceNames.toHashSet()
}
}
@@ -0,0 +1,54 @@
package org.apollo.game.plugin.testing.junit.params
import org.apollo.cache.def.ItemDefinition
import org.apollo.cache.def.NpcDefinition
import org.apollo.cache.def.ObjectDefinition
import org.junit.jupiter.params.provider.ArgumentsSource
/**
* `@ItemDefinitionSource` is an [ArgumentsSource] for [ItemDefinition]s.
*
* @param sourceNames The names of the properties or functions annotated with `@ItemDefinitions` to use as sources of
* [ItemDefinition]s for the test with this annotation. Every property/function must return
* `Collection<ItemDefinition>`. If no [sourceNames] are provided, every property and function annotated with
* `@ItemDefinitions` (in this class's companion object) will be used.
*
* @see org.junit.jupiter.params.provider.ArgumentsSource
* @see org.junit.jupiter.params.ParameterizedTest
*/
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
@ArgumentsSource(ItemDefinitionsProvider::class)
annotation class ItemDefinitionSource(vararg val sourceNames: String)
/**
* `@NpcDefinitionSource` is an [ArgumentsSource] for [NpcDefinition]s.
*
* @param sourceNames The names of the properties or functions annotated with `@NpcDefinitions` to use as sources of
* [NpcDefinition]s for the test with this annotation. Every property/function must return
* `Collection<NpcDefinition>`. If no [sourceNames] are provided, every property and function annotated with
* `@NpcDefinitions` (in this class's companion object) will be used.
*
* @see org.junit.jupiter.params.provider.ArgumentsSource
* @see org.junit.jupiter.params.ParameterizedTest
*/
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
@ArgumentsSource(NpcDefinitionsProvider::class)
annotation class NpcDefinitionSource(vararg val sourceNames: String)
/**
* `@ObjectDefinitionSource` is an [ArgumentsSource] for [ObjectDefinition]s.
*
* @param sourceNames The names of the properties or functions annotated with `@ObjectDefinitions` to use as sources of
* [ObjectDefinition]s for the test with this annotation. Every property/function must return
* `Collection<ObjectDefinition>`. If no [sourceNames] are provided, every property and function annotated with
* `@ObjectDefinitions` (in this class's companion object) will be used.
*
* @see org.junit.jupiter.params.provider.ArgumentsSource
* @see org.junit.jupiter.params.ParameterizedTest
*/
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
@ArgumentsSource(ObjectDefinitionsProvider::class)
annotation class ObjectDefinitionSource(vararg val sourceNames: String)