Support npc and object definitions in plugin tests

This commit is contained in:
Major
2018-08-21 00:47:23 +01:00
committed by Major-
parent 71158b3b5e
commit 86fba62ab9
10 changed files with 182 additions and 72 deletions
@@ -5,6 +5,8 @@ import io.mockk.slot
import io.mockk.spyk import io.mockk.spyk
import io.mockk.staticMockk import io.mockk.staticMockk
import org.apollo.cache.def.ItemDefinition 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.message.handler.MessageHandlerChainSet
import org.apollo.game.model.World import org.apollo.game.model.World
import org.apollo.game.model.entity.Npc 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.fakes.FakePluginContextFactory
import org.apollo.game.plugin.testing.junit.api.ActionCapture 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.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.apollo.game.plugin.testing.junit.mocking.StubPrototype
import org.junit.jupiter.api.extension.* import org.junit.jupiter.api.extension.AfterAllCallback
import java.util.* 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.KMutableProperty
import kotlin.reflect.full.companionObject
import kotlin.reflect.full.createType import kotlin.reflect.full.createType
import kotlin.reflect.full.declaredMemberFunctions import kotlin.reflect.full.declaredMemberFunctions
import kotlin.reflect.full.declaredMemberProperties import kotlin.reflect.full.declaredMemberProperties
import kotlin.reflect.full.findAnnotation import kotlin.reflect.jvm.isAccessible
import kotlin.reflect.jvm.jvmErasure import kotlin.reflect.jvm.jvmErasure
internal val supportedTestDoubleTypes = setOf( internal val supportedTestDoubleTypes = setOf(
@@ -67,6 +80,25 @@ class ApolloTestingExtension :
val stubHandlers = MessageHandlerChainSet() val stubHandlers = MessageHandlerChainSet()
val stubWorld = spyk(World()) 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<ItemDefinition, ItemDefinitions>(
testClassMethods, companionInstance, ItemDefinition::getId, ItemDefinition::lookup
)
createTestDefinitions<NpcDefinition, NpcDefinitions>(
testClassMethods, companionInstance, NpcDefinition::getId, NpcDefinition::lookup
)
createTestDefinitions<ObjectDefinition, ObjectDefinitions>(
testClassMethods, companionInstance, ObjectDefinition::getId, ObjectDefinition::lookup
)
}
val pluginEnvironment = KotlinPluginEnvironment(stubWorld) val pluginEnvironment = KotlinPluginEnvironment(stubWorld)
pluginEnvironment.setContext(FakePluginContextFactory.create(stubHandlers)) pluginEnvironment.setContext(FakePluginContextFactory.create(stubHandlers))
pluginEnvironment.load(ArrayList<PluginMetaData>()) pluginEnvironment.load(ArrayList<PluginMetaData>())
@@ -77,22 +109,8 @@ class ApolloTestingExtension :
} }
override fun beforeEach(context: ExtensionContext) { override fun beforeEach(context: ExtensionContext) {
val testClass = context.requiredTestClass.kotlin
val testClassInstance = context.requiredTestInstance val testClassInstance = context.requiredTestInstance
val testClassProps = testClass.declaredMemberProperties val testClassProps = context.requiredTestClass.kotlin.declaredMemberProperties
val testClassMethods = context.testClass.map { it.kotlin.declaredMemberFunctions }.orElse(emptyList())
val testClassItemDefs = testClassMethods.asSequence()
.mapNotNull { it.findAnnotation<ItemDefinitions>()?.let { anno -> it to anno } }
.flatMap { (it.first.call(context.requiredTestInstance as Any) as Collection<ItemDefinition>).asSequence() }
.map { it.id to it }
.toMap()
if (testClassItemDefs.isNotEmpty()) {
val itemIdSlot = slot<Int>()
staticMockk<ItemDefinition>().mock()
every { ItemDefinition.lookup(capture(itemIdSlot)) } answers { testClassItemDefs[itemIdSlot.captured] }
}
val store = context.getStore(namespace) val store = context.getStore(namespace)
val state = store.get(ApolloTestState::class) as ApolloTestState val state = store.get(ApolloTestState::class) as ApolloTestState
@@ -101,10 +119,10 @@ class ApolloTestingExtension :
.mapNotNull { it as? KMutableProperty<*> } .mapNotNull { it as? KMutableProperty<*> }
.filter { supportedTestDoubleTypes.contains(it.returnType) } .filter { supportedTestDoubleTypes.contains(it.returnType) }
propertyStubSites.forEach { propertyStubSites.forEach { property ->
it.setter.call( property.setter.call(
testClassInstance, 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())) 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 <reified D : Any, reified A : Annotation> createTestDefinitions(
testClassMethods: Collection<KFunction<*>>,
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<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()
}
.associateBy(idMapper)
if (testDefinitions.isNotEmpty()) {
val idSlot = slot<Int>()
staticMockk<D>().mock()
every { lookup(capture(idSlot)) } answers { testDefinitions[idSlot.captured]!! }
}
}
} }
@@ -1,5 +1,37 @@
package org.apollo.game.plugin.testing.junit.api.annotations 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<ItemDefinition>`.
*/
@Target(AnnotationTarget.FUNCTION) @Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME) @Retention(AnnotationRetention.RUNTIME)
annotation class ItemDefinitions 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<NpcDefinition>`.
*/
@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<ObjectDefinition>`.
*/
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class ObjectDefinitions
+5 -1
View File
@@ -22,7 +22,11 @@ object Definitions {
} }
fun npc(id: Int): NpcDefinition? { 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? { fun npc(name: String): NpcDefinition? {
@@ -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.Pickaxe
import org.apollo.game.plugin.skills.mining.TIN_OBJECTS import org.apollo.game.plugin.skills.mining.TIN_OBJECTS
import org.apollo.game.plugin.testing.assertions.after 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.assertions.verifyAfter
import org.apollo.game.plugin.testing.junit.ApolloTestingExtension 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.ActionCapture
import org.apollo.game.plugin.testing.junit.api.annotations.ItemDefinitions 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.annotations.TestMock
import org.apollo.game.plugin.testing.junit.api.interactions.spawnObject 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.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
@@ -26,14 +26,6 @@ import org.junit.jupiter.api.extension.ExtendWith
@ExtendWith(ApolloTestingExtension::class) @ExtendWith(ApolloTestingExtension::class)
class MiningActionTests { class MiningActionTests {
private val TIN_OBJ_IDS = TIN_OBJECTS.entries.first()
@ItemDefinitions
fun ores() = Ore.values()
.map { ItemDefinition(it.id).also { it.name = "<ore_type>" } }
@ItemDefinitions
fun pickaxes() = listOf(ItemDefinition(Pickaxe.BRONZE.id))
@TestMock @TestMock
lateinit var world: World lateinit var world: World
@@ -50,8 +42,12 @@ class MiningActionTests {
val target = spyk(MiningTarget(obj.id, obj.position, Ore.TIN)) val target = spyk(MiningTarget(obj.id, obj.position, Ore.TIN))
every { target.skillRequirementsMet(player) } returns false every { target.skillRequirementsMet(player) } returns false
player.startAction(MiningAction(player, Pickaxe.BRONZE, target)) 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 @Test
@@ -63,12 +59,15 @@ class MiningActionTests {
every { target.skillRequirementsMet(player) } returns true every { target.skillRequirementsMet(player) } returns true
every { target.isSuccessful(player, any()) } 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.skillSet.setCurrentLevel(Skill.MINING, Ore.TIN.level)
player.startAction(MiningAction(player, Pickaxe.BRONZE, target)) 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()) { after(action.complete()) {
verify { player.sendMessage("You manage to mine some <ore_type>") } verify { player.sendMessage("You manage to mine some <ore_type>") }
verify { world.expireObject(obj, expiredTinId, Ore.TIN.respawn) } verify { world.expireObject(obj, expiredTinId, Ore.TIN.respawn) }
@@ -77,4 +76,16 @@ class MiningActionTests {
assertEquals(player.skillSet.getExperience(Skill.MINING), Ore.TIN.exp) 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 = "<ore_type>" } }
@ItemDefinitions
fun pickaxes() = listOf(ItemDefinition(Pickaxe.BRONZE.id))
}
} }
@@ -13,11 +13,6 @@ import org.junit.jupiter.params.provider.EnumSource
@ExtendWith(ApolloTestingExtension::class) @ExtendWith(ApolloTestingExtension::class)
class PickaxeTests { class PickaxeTests {
@ItemDefinitions
fun pickaxes() = Pickaxe.values().map {
ItemDefinition(it.id).apply { isStackable = false }
}
@TestMock @TestMock
lateinit var player: Player lateinit var player: Player
@@ -72,4 +67,11 @@ class PickaxeTests {
assertEquals(pickaxe, Pickaxe.bestFor(player)) assertEquals(pickaxe, Pickaxe.bestFor(player))
} }
private companion object {
@ItemDefinitions
fun pickaxes() = Pickaxe.values().map {
ItemDefinition(it.id).apply { isStackable = false }
}
}
} }
@@ -2,13 +2,13 @@
import org.apollo.cache.def.ItemDefinition import org.apollo.cache.def.ItemDefinition
import org.apollo.game.model.entity.Player import org.apollo.game.model.entity.Player
import org.apollo.game.plugin.skills.mining.Ore 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.assertions.verifyAfter
import org.apollo.game.plugin.testing.junit.ApolloTestingExtension 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.ActionCapture
import org.apollo.game.plugin.testing.junit.api.annotations.ItemDefinitions 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.annotations.TestMock
import org.apollo.game.plugin.testing.junit.api.interactions.interactWithObject 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.api.extension.ExtendWith
import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ArgumentsSource import org.junit.jupiter.params.provider.ArgumentsSource
@@ -16,11 +16,6 @@ import org.junit.jupiter.params.provider.ArgumentsSource
@ExtendWith(ApolloTestingExtension::class) @ExtendWith(ApolloTestingExtension::class)
class ProspectingTests { class ProspectingTests {
@ItemDefinitions
fun ores() = Ore.values().map {
ItemDefinition(it.id).also { it.name = "<ore_type>" }
}
@TestMock @TestMock
lateinit var player: Player lateinit var player: Player
@@ -43,4 +38,12 @@ class ProspectingTests {
verifyAfter(action.complete()) { player.sendMessage(contains("no ore available in this rock")) } 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 = "<ore_type>" }
}
}
} }
@@ -26,11 +26,6 @@ class BuryBoneTests {
@TestMock @TestMock
lateinit var action: ActionCapture lateinit var action: ActionCapture
@ItemDefinitions
fun bones(): Collection<ItemDefinition> {
return Bone.values().map { ItemDefinition(it.id) }
}
@ParameterizedTest @ParameterizedTest
@EnumSource(value = Bone::class) @EnumSource(value = Bone::class)
fun `Burying a bone should send a message`(bone: Bone) { fun `Burying a bone should send a message`(bone: Bone) {
@@ -69,4 +64,11 @@ class BuryBoneTests {
} }
} }
private companion object {
@ItemDefinitions
fun bones(): Collection<ItemDefinition> {
return Bone.values().map { ItemDefinition(it.id) }
}
}
} }
@@ -13,11 +13,6 @@ import org.junit.jupiter.params.provider.EnumSource
@ExtendWith(ApolloTestingExtension::class) @ExtendWith(ApolloTestingExtension::class)
class AxeTests { class AxeTests {
@ItemDefinitions
fun axes() = Axe.values().map {
ItemDefinition(it.id).apply { isStackable = false }
}
@TestMock @TestMock
lateinit var player: Player lateinit var player: Player
@@ -69,4 +64,11 @@ class AxeTests {
assertEquals(axe, Axe.bestFor(player)) assertEquals(axe, Axe.bestFor(player))
} }
private companion object {
@ItemDefinitions
fun axes() = Axe.values().map {
ItemDefinition(it.id).apply { isStackable = false }
}
}
} }
@@ -20,19 +20,11 @@ import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.extension.ExtendWith import org.junit.jupiter.api.extension.ExtendWith
import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ArgumentsSource import org.junit.jupiter.params.provider.ArgumentsSource
import java.util.* import java.util.Random
@ExtendWith(ApolloTestingExtension::class) @ExtendWith(ApolloTestingExtension::class)
class WoodcuttingTests { class WoodcuttingTests {
@ItemDefinitions
fun logs() = woodcuttingTestData().map {
ItemDefinition(it.tree.id).also { it.name = "<tree_type>" }
}
@ItemDefinitions
fun tools() = listOf(ItemDefinition(Axe.BRONZE.id))
@TestMock @TestMock
lateinit var action: ActionCapture lateinit var action: ActionCapture
@@ -85,4 +77,14 @@ class WoodcuttingTests {
assertEquals(1, player.inventory.getAmount(data.tree.id)) assertEquals(1, player.inventory.getAmount(data.tree.id))
} }
} }
private companion object {
@ItemDefinitions
fun logs() = woodcuttingTestData().map {
ItemDefinition(it.tree.id).also { it.name = "<tree_type>" }
}
@ItemDefinitions
fun tools() = listOf(ItemDefinition(Axe.BRONZE.id))
}
} }
@@ -1,5 +1,11 @@
package org.apollo.game.plugin; 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.ClassGraph;
import io.github.classgraph.ClassInfo; import io.github.classgraph.ClassInfo;
import io.github.classgraph.ClassInfoList; import io.github.classgraph.ClassInfoList;
@@ -7,12 +13,6 @@ import io.github.classgraph.ScanResult;
import org.apollo.game.model.World; import org.apollo.game.model.World;
import org.apollo.game.plugin.kotlin.KotlinPluginScript; 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 { public class KotlinPluginEnvironment implements PluginEnvironment {
private static final Logger logger = Logger.getLogger(KotlinPluginEnvironment.class.getName()); private static final Logger logger = Logger.getLogger(KotlinPluginEnvironment.class.getName());
@@ -28,7 +28,6 @@ public class KotlinPluginEnvironment implements PluginEnvironment {
@Override @Override
public void load(Collection<PluginMetaData> plugins) { public void load(Collection<PluginMetaData> plugins) {
List<KotlinPluginScript> pluginScripts = new ArrayList<>(); List<KotlinPluginScript> pluginScripts = new ArrayList<>();
List<Class<? extends KotlinPluginScript>> pluginClasses = new ArrayList<>();
ClassGraph classGraph = new ClassGraph().enableAllInfo(); ClassGraph classGraph = new ClassGraph().enableAllInfo();