diff --git a/game/plugin-testing/build.gradle b/game/plugin-testing/build.gradle index ae3a276c..0f2dc14d 100644 --- a/game/plugin-testing/build.gradle +++ b/game/plugin-testing/build.gradle @@ -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" + } } \ No newline at end of file diff --git a/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/ApolloTestExtension.kt b/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/ApolloTestExtension.kt index defeb83f..c9942e41 100644 --- a/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/ApolloTestExtension.kt +++ b/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/ApolloTestExtension.kt @@ -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> = companion.declaredMemberFunctions + companion.declaredMemberProperties createTestDefinitions( - testClassMethods, companionInstance, ItemDefinition::getId, ItemDefinition::lookup + callables, companionInstance, ItemDefinition::getId, ItemDefinition::lookup, + ItemDefinition::getDefinitions ) createTestDefinitions( - testClassMethods, companionInstance, NpcDefinition::getId, NpcDefinition::lookup + callables, companionInstance, NpcDefinition::getId, NpcDefinition::lookup, + NpcDefinition::getDefinitions ) createTestDefinitions( - 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()) + 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 createTestDefinitions( - testClassMethods: Collection>, + testClassMethods: Collection>, companionObjectInstance: Any?, crossinline idMapper: (D) -> Int, - crossinline lookup: (Int) -> D + crossinline lookup: (Int) -> D?, + crossinline getAll: () -> Array ) { - val testDefinitions = testClassMethods.asSequence() - .filter { method -> method.annotations.any { it is A } } - .flatMap { method -> - @Suppress("UNCHECKED_CAST") - method as? KFunction> ?: 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(testClassMethods, companionObjectInstance) .associateBy(idMapper) if (testDefinitions.isNotEmpty()) { val idSlot = slot() - staticMockk().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 findTestDefinitions( + callables: Collection>, + companionObjectInstance: Any? + ): List { + return callables + .filter { method -> method.annotations.any { it is A } } + .flatMap { method -> + @Suppress("UNCHECKED_CAST") + method as? KCallable> ?: 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) + } + } + + } + } \ No newline at end of file diff --git a/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/api/annotations/definitionAnnotations.kt b/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/api/annotations/DefinitionAnnotations.kt similarity index 86% rename from game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/api/annotations/definitionAnnotations.kt rename to game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/api/annotations/DefinitionAnnotations.kt index b730b277..b3d6c8d6 100644 --- a/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/api/annotations/definitionAnnotations.kt +++ b/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/api/annotations/DefinitionAnnotations.kt @@ -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`. */ -@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`. */ -@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`. */ -@Target(AnnotationTarget.FUNCTION) +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY) @Retention(AnnotationRetention.RUNTIME) annotation class ObjectDefinitions \ No newline at end of file diff --git a/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/params/DefinitionProviders.kt b/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/params/DefinitionProviders.kt new file mode 100644 index 00000000..5edfd5eb --- /dev/null +++ b/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/params/DefinitionProviders.kt @@ -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( + private val definitionProvider: (methods: Collection>, companionObjectInstance: Any) -> List +) : ArgumentsProvider { + + protected lateinit var sourceNames: Set + + override fun provideArguments(context: ExtensionContext): Stream { + 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> = 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( + { methods, companion -> findTestDefinitions(methods, companion) } +), AnnotationConsumer { + + 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( + { methods, companion -> findTestDefinitions(methods, companion) } +), AnnotationConsumer { + + 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( + { methods, companion -> findTestDefinitions(methods, companion) } +), AnnotationConsumer { + + override fun accept(source: ObjectDefinitionSource) { + sourceNames = source.sourceNames.toHashSet() + } + +} diff --git a/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/params/DefinitionSource.kt b/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/params/DefinitionSource.kt new file mode 100644 index 00000000..c97fc9ca --- /dev/null +++ b/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/params/DefinitionSource.kt @@ -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`. 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`. 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`. 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) diff --git a/game/plugin/api/src/org/apollo/game/plugin/api/definitions.kt b/game/plugin/api/src/org/apollo/game/plugin/api/Definitions.kt similarity index 100% rename from game/plugin/api/src/org/apollo/game/plugin/api/definitions.kt rename to game/plugin/api/src/org/apollo/game/plugin/api/Definitions.kt