From 86fba62ab989e12f814bb80baeb8e91372d6b78a Mon Sep 17 00:00:00 2001 From: Major Date: Tue, 21 Aug 2018 00:47:23 +0100 Subject: [PATCH] Support npc and object definitions in plugin tests --- .../testing/junit/ApolloTestExtension.kt | 95 +++++++++++++++---- .../api/annotations/definitionAnnotations.kt | 34 ++++++- game/plugin/api/src/definitions.kt | 6 +- .../skills/mining/test/MiningActionTests.kt | 35 ++++--- .../plugin/skills/mining/test/PickaxeTests.kt | 12 ++- .../skills/mining/test/ProspectingTests.kt | 15 +-- .../skills/prayer/test/BuryBoneTests.kt | 12 ++- .../skills/woodcutting/test/AxeTests.kt | 12 ++- .../woodcutting/test/WoodcuttingTests.kt | 20 ++-- .../game/plugin/KotlinPluginEnvironment.java | 13 ++- 10 files changed, 182 insertions(+), 72 deletions(-) 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 b3640465..defeb83f 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 @@ -5,6 +5,8 @@ import io.mockk.slot import io.mockk.spyk import io.mockk.staticMockk import org.apollo.cache.def.ItemDefinition +import org.apollo.cache.def.NpcDefinition +import org.apollo.cache.def.ObjectDefinition import org.apollo.game.message.handler.MessageHandlerChainSet import org.apollo.game.model.World import org.apollo.game.model.entity.Npc @@ -15,14 +17,25 @@ 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.* -import java.util.* +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 kotlin.reflect.KMutableProperty +import kotlin.reflect.full.companionObject import kotlin.reflect.full.createType import kotlin.reflect.full.declaredMemberFunctions import kotlin.reflect.full.declaredMemberProperties -import kotlin.reflect.full.findAnnotation +import kotlin.reflect.jvm.isAccessible import kotlin.reflect.jvm.jvmErasure internal val supportedTestDoubleTypes = setOf( @@ -67,6 +80,25 @@ class ApolloTestingExtension : val stubHandlers = MessageHandlerChainSet() val stubWorld = spyk(World()) + context.testClass // This _must_ come before plugin environment initialisation + .map { it.kotlin.companionObject } + .ifPresent { companion -> + val companionInstance = companion.objectInstance!! + val testClassMethods = companion.declaredMemberFunctions + + createTestDefinitions( + testClassMethods, companionInstance, ItemDefinition::getId, ItemDefinition::lookup + ) + + createTestDefinitions( + testClassMethods, companionInstance, NpcDefinition::getId, NpcDefinition::lookup + ) + + createTestDefinitions( + testClassMethods, companionInstance, ObjectDefinition::getId, ObjectDefinition::lookup + ) + } + val pluginEnvironment = KotlinPluginEnvironment(stubWorld) pluginEnvironment.setContext(FakePluginContextFactory.create(stubHandlers)) pluginEnvironment.load(ArrayList()) @@ -77,22 +109,8 @@ class ApolloTestingExtension : } override fun beforeEach(context: ExtensionContext) { - val testClass = context.requiredTestClass.kotlin val testClassInstance = context.requiredTestInstance - val testClassProps = testClass.declaredMemberProperties - val testClassMethods = context.testClass.map { it.kotlin.declaredMemberFunctions }.orElse(emptyList()) - val testClassItemDefs = testClassMethods.asSequence() - .mapNotNull { it.findAnnotation()?.let { anno -> it to anno } } - .flatMap { (it.first.call(context.requiredTestInstance as Any) as Collection).asSequence() } - .map { it.id to it } - .toMap() - - if (testClassItemDefs.isNotEmpty()) { - val itemIdSlot = slot() - - staticMockk().mock() - every { ItemDefinition.lookup(capture(itemIdSlot)) } answers { testClassItemDefs[itemIdSlot.captured] } - } + val testClassProps = context.requiredTestClass.kotlin.declaredMemberProperties val store = context.getStore(namespace) val state = store.get(ApolloTestState::class) as ApolloTestState @@ -101,10 +119,10 @@ class ApolloTestingExtension : .mapNotNull { it as? KMutableProperty<*> } .filter { supportedTestDoubleTypes.contains(it.returnType) } - propertyStubSites.forEach { - it.setter.call( + propertyStubSites.forEach { property -> + property.setter.call( testClassInstance, - state.createStub(StubPrototype(it.returnType.jvmErasure, it.annotations)) + state.createStub(StubPrototype(property.returnType.jvmErasure, property.annotations)) ) } } @@ -124,4 +142,39 @@ class ApolloTestingExtension : return testState.createStub(StubPrototype(paramType, param.annotations.toList())) } + + /** + * Mocks the definition class of type [D] for any function with the attached annotation [A]. + * + * @param testClassMethods All of the methods in the class being tested. + * @param idMapper The map function that returns an id given a definition [D]. + * @param lookup The lookup function that returns an instance of [D] given a definition id. + */ + private inline fun createTestDefinitions( + testClassMethods: Collection>, + companionObjectInstance: Any?, + crossinline idMapper: (D) -> Int, + crossinline lookup: (Int) -> D + ) { + 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() + } + .associateBy(idMapper) + + if (testDefinitions.isNotEmpty()) { + val idSlot = slot() + + staticMockk().mock() + every { lookup(capture(idSlot)) } answers { testDefinitions[idSlot.captured]!! } + } + } + } \ 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 index 484d0b2c..b730b277 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 @@ -1,5 +1,37 @@ package org.apollo.game.plugin.testing.junit.api.annotations +/** + * Specifies that the the ItemDefinitions returned by the annotated function should be inserted into the definition + * table. + * + * The annotated function **must**: + * - Be inside a **companion object** inside an apollo test class (a regular object will not work). + * - Return a `Collection`. + */ @Target(AnnotationTarget.FUNCTION) @Retention(AnnotationRetention.RUNTIME) -annotation class ItemDefinitions \ No newline at end of file +annotation class ItemDefinitions + +/** + * Specifies that the the NpcDefinitions returned by the annotated function should be inserted into the definition + * table. + * + * The annotated function **must**: + * - Be inside a **companion object** inside an apollo test class (a regular object will not work). + * - Return a `Collection`. + */ +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +annotation class NpcDefinitions + +/** + * Specifies that the the ObjectDefinitions returned by the annotated function should be inserted into the definition + * table. + * + * The annotated function **must**: + * - Be inside a **companion object** inside an apollo test class (a regular object will not work). + * - Return a `Collection`. + */ +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +annotation class ObjectDefinitions \ No newline at end of file diff --git a/game/plugin/api/src/definitions.kt b/game/plugin/api/src/definitions.kt index b2f826e5..9e64e460 100644 --- a/game/plugin/api/src/definitions.kt +++ b/game/plugin/api/src/definitions.kt @@ -22,7 +22,11 @@ object Definitions { } fun npc(id: Int): NpcDefinition? { - return NpcDefinition.lookup(id) + try { + return NpcDefinition.lookup(id) + } catch (e: NullPointerException) { + throw RuntimeException("Failed to find npc $id: count=${NpcDefinition.count()}") + } } fun npc(name: String): NpcDefinition? { diff --git a/game/plugin/skills/mining/test/MiningActionTests.kt b/game/plugin/skills/mining/test/MiningActionTests.kt index a378f0ca..f9fe0319 100644 --- a/game/plugin/skills/mining/test/MiningActionTests.kt +++ b/game/plugin/skills/mining/test/MiningActionTests.kt @@ -12,13 +12,13 @@ import org.apollo.game.plugin.skills.mining.Ore import org.apollo.game.plugin.skills.mining.Pickaxe import org.apollo.game.plugin.skills.mining.TIN_OBJECTS import org.apollo.game.plugin.testing.assertions.after +import org.apollo.game.plugin.testing.assertions.contains import org.apollo.game.plugin.testing.assertions.verifyAfter import org.apollo.game.plugin.testing.junit.ApolloTestingExtension 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.TestMock import org.apollo.game.plugin.testing.junit.api.interactions.spawnObject -import org.apollo.game.plugin.testing.assertions.contains import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test @@ -26,14 +26,6 @@ import org.junit.jupiter.api.extension.ExtendWith @ExtendWith(ApolloTestingExtension::class) class MiningActionTests { - private val TIN_OBJ_IDS = TIN_OBJECTS.entries.first() - - @ItemDefinitions - fun ores() = Ore.values() - .map { ItemDefinition(it.id).also { it.name = "" } } - - @ItemDefinitions - fun pickaxes() = listOf(ItemDefinition(Pickaxe.BRONZE.id)) @TestMock lateinit var world: World @@ -50,8 +42,12 @@ class MiningActionTests { val target = spyk(MiningTarget(obj.id, obj.position, Ore.TIN)) every { target.skillRequirementsMet(player) } returns false + player.startAction(MiningAction(player, Pickaxe.BRONZE, target)) - verifyAfter(action.complete()) { player.sendMessage(contains("do not have the required level")) } + + verifyAfter(action.complete()) { + player.sendMessage(contains("do not have the required level")) + } } @Test @@ -63,12 +59,15 @@ class MiningActionTests { every { target.skillRequirementsMet(player) } returns true every { target.isSuccessful(player, any()) } returns true - every { world.expireObject(obj, any(), any()) } answers {} + every { world.expireObject(obj, any(), any()) } answers { } player.skillSet.setCurrentLevel(Skill.MINING, Ore.TIN.level) player.startAction(MiningAction(player, Pickaxe.BRONZE, target)) - verifyAfter(action.ticks(1)) { player.sendMessage(contains("You swing your pick")) } + verifyAfter(action.ticks(1)) { + player.sendMessage(contains("You swing your pick")) + } + after(action.complete()) { verify { player.sendMessage("You manage to mine some ") } verify { world.expireObject(obj, expiredTinId, Ore.TIN.respawn) } @@ -77,4 +76,16 @@ class MiningActionTests { assertEquals(player.skillSet.getExperience(Skill.MINING), Ore.TIN.exp) } } + + private companion object { + private val TIN_OBJ_IDS = TIN_OBJECTS.entries.first() + + @ItemDefinitions + fun ores() = Ore.values() + .map { ItemDefinition(it.id).apply { name = "" } } + + @ItemDefinitions + fun pickaxes() = listOf(ItemDefinition(Pickaxe.BRONZE.id)) + } + } \ No newline at end of file diff --git a/game/plugin/skills/mining/test/PickaxeTests.kt b/game/plugin/skills/mining/test/PickaxeTests.kt index 15198c0d..69e37539 100644 --- a/game/plugin/skills/mining/test/PickaxeTests.kt +++ b/game/plugin/skills/mining/test/PickaxeTests.kt @@ -13,11 +13,6 @@ import org.junit.jupiter.params.provider.EnumSource @ExtendWith(ApolloTestingExtension::class) class PickaxeTests { - @ItemDefinitions - fun pickaxes() = Pickaxe.values().map { - ItemDefinition(it.id).apply { isStackable = false } - } - @TestMock lateinit var player: Player @@ -72,4 +67,11 @@ class PickaxeTests { assertEquals(pickaxe, Pickaxe.bestFor(player)) } + private companion object { + @ItemDefinitions + fun pickaxes() = Pickaxe.values().map { + ItemDefinition(it.id).apply { isStackable = false } + } + } + } \ No newline at end of file diff --git a/game/plugin/skills/mining/test/ProspectingTests.kt b/game/plugin/skills/mining/test/ProspectingTests.kt index 0274951d..2bbc0766 100644 --- a/game/plugin/skills/mining/test/ProspectingTests.kt +++ b/game/plugin/skills/mining/test/ProspectingTests.kt @@ -2,13 +2,13 @@ import org.apollo.cache.def.ItemDefinition import org.apollo.game.model.entity.Player import org.apollo.game.plugin.skills.mining.Ore +import org.apollo.game.plugin.testing.assertions.contains import org.apollo.game.plugin.testing.assertions.verifyAfter import org.apollo.game.plugin.testing.junit.ApolloTestingExtension 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.TestMock import org.apollo.game.plugin.testing.junit.api.interactions.interactWithObject -import org.apollo.game.plugin.testing.assertions.contains import org.junit.jupiter.api.extension.ExtendWith import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.ArgumentsSource @@ -16,11 +16,6 @@ import org.junit.jupiter.params.provider.ArgumentsSource @ExtendWith(ApolloTestingExtension::class) class ProspectingTests { - @ItemDefinitions - fun ores() = Ore.values().map { - ItemDefinition(it.id).also { it.name = "" } - } - @TestMock lateinit var player: Player @@ -43,4 +38,12 @@ class ProspectingTests { verifyAfter(action.complete()) { player.sendMessage(contains("no ore available in this rock")) } } + + private companion object { + @ItemDefinitions + fun ores() = Ore.values().map { + ItemDefinition(it.id).also { it.name = "" } + } + } + } \ No newline at end of file diff --git a/game/plugin/skills/prayer/test/BuryBoneTests.kt b/game/plugin/skills/prayer/test/BuryBoneTests.kt index d5aaae14..6d485334 100644 --- a/game/plugin/skills/prayer/test/BuryBoneTests.kt +++ b/game/plugin/skills/prayer/test/BuryBoneTests.kt @@ -26,11 +26,6 @@ class BuryBoneTests { @TestMock lateinit var action: ActionCapture - @ItemDefinitions - fun bones(): Collection { - return Bone.values().map { ItemDefinition(it.id) } - } - @ParameterizedTest @EnumSource(value = Bone::class) fun `Burying a bone should send a message`(bone: Bone) { @@ -69,4 +64,11 @@ class BuryBoneTests { } } + private companion object { + @ItemDefinitions + fun bones(): Collection { + return Bone.values().map { ItemDefinition(it.id) } + } + } + } \ No newline at end of file diff --git a/game/plugin/skills/woodcutting/test/AxeTests.kt b/game/plugin/skills/woodcutting/test/AxeTests.kt index 2b2d3a2b..cbbedb55 100644 --- a/game/plugin/skills/woodcutting/test/AxeTests.kt +++ b/game/plugin/skills/woodcutting/test/AxeTests.kt @@ -13,11 +13,6 @@ import org.junit.jupiter.params.provider.EnumSource @ExtendWith(ApolloTestingExtension::class) class AxeTests { - @ItemDefinitions - fun axes() = Axe.values().map { - ItemDefinition(it.id).apply { isStackable = false } - } - @TestMock lateinit var player: Player @@ -69,4 +64,11 @@ class AxeTests { assertEquals(axe, Axe.bestFor(player)) } + private companion object { + @ItemDefinitions + fun axes() = Axe.values().map { + ItemDefinition(it.id).apply { isStackable = false } + } + } + } \ No newline at end of file diff --git a/game/plugin/skills/woodcutting/test/WoodcuttingTests.kt b/game/plugin/skills/woodcutting/test/WoodcuttingTests.kt index 8c8f1cd4..c1a6e1ad 100644 --- a/game/plugin/skills/woodcutting/test/WoodcuttingTests.kt +++ b/game/plugin/skills/woodcutting/test/WoodcuttingTests.kt @@ -20,19 +20,11 @@ import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.extension.ExtendWith import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.ArgumentsSource -import java.util.* +import java.util.Random @ExtendWith(ApolloTestingExtension::class) class WoodcuttingTests { - @ItemDefinitions - fun logs() = woodcuttingTestData().map { - ItemDefinition(it.tree.id).also { it.name = "" } - } - - @ItemDefinitions - fun tools() = listOf(ItemDefinition(Axe.BRONZE.id)) - @TestMock lateinit var action: ActionCapture @@ -85,4 +77,14 @@ class WoodcuttingTests { assertEquals(1, player.inventory.getAmount(data.tree.id)) } } + + private companion object { + @ItemDefinitions + fun logs() = woodcuttingTestData().map { + ItemDefinition(it.tree.id).also { it.name = "" } + } + + @ItemDefinitions + fun tools() = listOf(ItemDefinition(Axe.BRONZE.id)) + } } \ No newline at end of file diff --git a/game/src/main/java/org/apollo/game/plugin/KotlinPluginEnvironment.java b/game/src/main/java/org/apollo/game/plugin/KotlinPluginEnvironment.java index d86cd0ed..7e8d61fa 100644 --- a/game/src/main/java/org/apollo/game/plugin/KotlinPluginEnvironment.java +++ b/game/src/main/java/org/apollo/game/plugin/KotlinPluginEnvironment.java @@ -1,5 +1,11 @@ package org.apollo.game.plugin; +import java.lang.reflect.Constructor; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.logging.Logger; + import io.github.classgraph.ClassGraph; import io.github.classgraph.ClassInfo; import io.github.classgraph.ClassInfoList; @@ -7,12 +13,6 @@ import io.github.classgraph.ScanResult; import org.apollo.game.model.World; import org.apollo.game.plugin.kotlin.KotlinPluginScript; -import java.lang.reflect.Constructor; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.logging.Logger; - public class KotlinPluginEnvironment implements PluginEnvironment { private static final Logger logger = Logger.getLogger(KotlinPluginEnvironment.class.getName()); @@ -28,7 +28,6 @@ public class KotlinPluginEnvironment implements PluginEnvironment { @Override public void load(Collection plugins) { List pluginScripts = new ArrayList<>(); - List> pluginClasses = new ArrayList<>(); ClassGraph classGraph = new ClassGraph().enableAllInfo();