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.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<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)
pluginEnvironment.setContext(FakePluginContextFactory.create(stubHandlers))
pluginEnvironment.load(ArrayList<PluginMetaData>())
@@ -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<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 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 <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
/**
* 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)
@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? {
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? {
@@ -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 = "<ore_type>" } }
@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 <ore_type>") }
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 = "<ore_type>" } }
@ItemDefinitions
fun pickaxes() = listOf(ItemDefinition(Pickaxe.BRONZE.id))
}
}
@@ -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 }
}
}
}
@@ -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 = "<ore_type>" }
}
@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 = "<ore_type>" }
}
}
}
@@ -26,11 +26,6 @@ class BuryBoneTests {
@TestMock
lateinit var action: ActionCapture
@ItemDefinitions
fun bones(): Collection<ItemDefinition> {
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<ItemDefinition> {
return Bone.values().map { ItemDefinition(it.id) }
}
}
}
@@ -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 }
}
}
}
@@ -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 = "<tree_type>" }
}
@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 = "<tree_type>" }
}
@ItemDefinitions
fun tools() = listOf(ItemDefinition(Axe.BRONZE.id))
}
}
@@ -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<PluginMetaData> plugins) {
List<KotlinPluginScript> pluginScripts = new ArrayList<>();
List<Class<? extends KotlinPluginScript>> pluginClasses = new ArrayList<>();
ClassGraph classGraph = new ClassGraph().enableAllInfo();