Update plugin test framework to junit5

Updates the testing infrastructure to use the latest relesae of junit and
leverages the new extension mechanism to create an easy to use testing
framework.  Also adds additional test coverage for several plugins.
This commit is contained in:
Gary Tierney
2018-08-19 22:28:41 +01:00
parent e255bd195e
commit 248a7d97d9
56 changed files with 1327 additions and 463 deletions
+8 -2
View File
@@ -5,9 +5,15 @@ dependencies {
api project(':game')
api project(':net')
api group: 'junit', name: 'junit', version: junitVersion
api group: 'org.powermock', name: 'powermock-api-mockito', version: powermockVersion
// JUnit Jupiter API and TestEngine implementation
api("org.junit.jupiter:junit-jupiter-api:${junitJupiterVersion}")
api("org.junit.jupiter:junit-jupiter-params:${junitJupiterVersion}")
implementation("org.junit.jupiter:junit-jupiter-engine:${junitJupiterVersion}")
implementation("org.junit.platform:junit-platform-launcher:${junitPlatformVersion}")
api group: 'io.mockk', name: 'mockk', version: mockkVersion
api group: 'org.assertj', name: 'assertj-core', version: assertjVersion
api group: 'com.willowtreeapps.assertk', name: 'assertk', version: assertkVersion
implementation group: 'org.powermock', name: 'powermock-module-junit4', version: powermockVersion
}
@@ -1,34 +0,0 @@
package org.apollo.game.plugin.testing
import org.apollo.game.message.handler.MessageHandlerChainSet
import org.apollo.game.model.World
import org.apollo.game.model.entity.Player
import org.apollo.game.plugin.*
import org.apollo.game.plugin.testing.fakes.FakePluginContextFactory
import org.junit.Before
import org.junit.runner.RunWith
import org.powermock.api.mockito.PowerMockito
import org.powermock.core.classloader.annotations.PrepareForTest
import org.powermock.modules.junit4.PowerMockRunner
import java.util.*
@RunWith(PowerMockRunner::class)
@PrepareForTest(World::class, PluginContext::class, Player::class)
abstract class KotlinPluginTest: KotlinPluginTestHelpers() {
override lateinit var world: World
override lateinit var player: Player
override lateinit var messageHandlers: MessageHandlerChainSet
@Before
open fun setup() {
messageHandlers = MessageHandlerChainSet()
world = PowerMockito.spy(World())
val pluginEnvironment = KotlinPluginEnvironment(world)
pluginEnvironment.setContext(FakePluginContextFactory.create(messageHandlers))
pluginEnvironment.load(ArrayList<PluginMetaData>())
player = world.spawnPlayer("testPlayer")
}
}
@@ -1,121 +0,0 @@
package org.apollo.game.plugin.testing
import org.apollo.cache.def.ItemDefinition
import org.apollo.cache.def.NpcDefinition
import org.apollo.game.action.Action
import org.apollo.game.message.handler.MessageHandlerChainSet
import org.apollo.game.message.impl.*
import org.apollo.game.model.*
import org.apollo.game.model.entity.*
import org.apollo.game.model.entity.obj.GameObject
import org.apollo.game.model.entity.obj.StaticGameObject
import org.apollo.net.message.Message
import org.apollo.util.security.PlayerCredentials
import org.junit.Assert
import org.mockito.*
import org.powermock.api.mockito.PowerMockito
/**
* A base class containing a set of helper methods to be used within plugin tests.
*/
abstract class KotlinPluginTestHelpers {
abstract var world: World
abstract var player: Player
abstract var messageHandlers: MessageHandlerChainSet
/**
* Waits for an [Action] to complete within a specified number of pulses, and with an optional predicate
* to test the [Action] against.
*/
fun Player.waitForActionCompletion(predicate: (Action<Player>) -> Boolean = { _ -> true }, timeout: Int = 15) {
val actionCaptor: ArgumentCaptor<Action<*>> = ArgumentCaptor.forClass(Action::class.java)
Mockito.verify(this).startAction(actionCaptor.capture())
val action: Action<Player> = actionCaptor.value as Action<Player>
Assert.assertTrue("Found wrong action type", predicate.invoke(action))
var pulses = 0
do {
action.pulse()
/**
* Introducing an artificial delay is necessary to prevent the timeout being exceeded before
* an asynchronous [Action] really starts. When a job is submitted to a new coroutine context
* there may be a delay before it is actually executed.
*
* This delay is typically sub-millisecond and is only incurred with startup. Since game actions
* have larger delays of their own this isn't a problem in practice.
*/
Thread.sleep(50L)
} while (action.isRunning && pulses++ < timeout)
Assert.assertFalse("Exceeded timeout waiting for action completion", pulses > timeout)
}
/**
* Spawns a new NPC with the minimum set of dependencies required to function correctly in the world.
*/
fun World.spawnNpc(id: Int, position: Position): Npc {
val definition = NpcDefinition(id)
val npc = Npc(this, position, definition, arrayOfNulls(4))
val region = regionRepository.fromPosition(position)
val npcs = npcRepository
npcs.add(npc)
region.addEntity(npc)
return npc
}
/**
* Spawn a new player stub in the world, with a dummy game session.
*/
fun World.spawnPlayer(username: String, position: Position = Position(3200, 3200, 0)): Player {
val credentials = PlayerCredentials(username, "test", 1, 1, "0.0.0.0")
val region = regionRepository.fromPosition(position)
val player = PowerMockito.spy(Player(this, credentials, position))
register(player)
region.addEntity(player)
PowerMockito.doNothing().`when`(player).send(Matchers.any())
return player
}
/**
* Spawn a new static game object into the world with the given id and position.
*/
fun World.spawnObject(id: Int, position: Position): GameObject {
val obj = StaticGameObject(this, id, position, 0, 0)
spawn(obj)
return obj
}
/**
* Fake a client [Message] originating from a player and send it to the relevant
* message handlers.
*/
fun Player.notify(message: Message) {
messageHandlers.notify(this, message)
}
/**
* Move the player within interaction distance to the given [Entity] and fake an action
* message.
*/
fun Player.interactWith(entity: Entity, option: Int = 1) {
position = entity.position.step(1, Direction.NORTH)
when (entity) {
is GameObject -> notify(ObjectActionMessage(option, entity.id, entity.position))
is Npc -> notify(NpcActionMessage(option, entity.index))
is Player -> notify(PlayerActionMessage(option, entity.index))
}
}
}
@@ -0,0 +1,21 @@
package org.apollo.game.plugin.testing.assertions
import io.mockk.MockKVerificationScope
import io.mockk.verify
import org.apollo.game.plugin.testing.junit.api.ActionCaptureCallbackRegistration
/**
* Verify some expectations on a [mock] after a delayed event (specified by [DelayMode]).
*/
fun verifyAfter(registration: ActionCaptureCallbackRegistration, description: String? = null, verifier: MockKVerificationScope.() -> Unit) {
after(registration, description) { verify(verifyBlock = verifier) }
}
/**
* Run a [callback] after a given delay, specified by [DelayMode].
*/
fun after(registration: ActionCaptureCallbackRegistration, description: String? = null, callback: () -> Unit) {
registration.function = callback
registration.description = description
}
@@ -0,0 +1,7 @@
package org.apollo.game.plugin.testing.assertions
import io.mockk.MockKMatcherScope
inline fun MockKMatcherScope.contains(search: String) = match<String> { it.contains(search) }
inline fun MockKMatcherScope.startsWith(search: String) = match<String> { it.startsWith(search) }
inline fun MockKMatcherScope.endsWith(search: String) = match<String> { it.endsWith(search) }
@@ -1,21 +1,25 @@
package org.apollo.game.plugin.testing.fakes
import io.mockk.every
import io.mockk.mockk
import io.mockk.slot
import org.apollo.game.message.handler.MessageHandler
import org.apollo.game.message.handler.MessageHandlerChainSet
import org.apollo.game.plugin.PluginContext
import org.apollo.net.message.Message
import org.mockito.invocation.InvocationOnMock
import org.mockito.stubbing.Answer
import org.powermock.api.mockito.PowerMockito
object FakePluginContextFactory {
fun create(messageHandlers: MessageHandlerChainSet): PluginContext {
val answer = Answer<Any?> { invocation: InvocationOnMock ->
messageHandlers.putHandler(
invocation.arguments[0] as Class<Message>,
invocation.arguments[1] as MessageHandler<*>)
val ctx = mockk<PluginContext>()
val typeCapture = slot<Class<Message>>()
val handlerCapture = slot<MessageHandler<Message>>()
every {
ctx.addMessageHandler(capture(typeCapture), capture(handlerCapture))
} answers {
messageHandlers.putHandler(typeCapture.captured, handlerCapture.captured)
}
return PowerMockito.mock(PluginContext::class.java, answer)
return ctx
}
}
@@ -0,0 +1,127 @@
package org.apollo.game.plugin.testing.junit
import io.mockk.every
import io.mockk.slot
import io.mockk.spyk
import io.mockk.staticMockk
import org.apollo.cache.def.ItemDefinition
import org.apollo.game.message.handler.MessageHandlerChainSet
import org.apollo.game.model.World
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.mocking.StubPrototype
import org.junit.jupiter.api.extension.*
import java.util.*
import kotlin.reflect.KMutableProperty
import kotlin.reflect.full.createType
import kotlin.reflect.full.declaredMemberFunctions
import kotlin.reflect.full.declaredMemberProperties
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.jvm.jvmErasure
internal val supportedTestDoubleTypes = setOf(
Player::class.createType(),
Npc::class.createType(),
GameObject::class.createType(),
World::class.createType(),
ActionCapture::class.createType()
)
class ApolloTestingExtension :
AfterTestExecutionCallback,
BeforeAllCallback,
AfterAllCallback,
BeforeEachCallback,
AfterEachCallback,
ParameterResolver {
private val namespace = ExtensionContext.Namespace.create("apollo")
private fun cleanup(context: ExtensionContext) {
val store = context.getStore(namespace)
val state = store.get(ApolloTestState::class) as ApolloTestState
try {
state.actionCapture?.runAction()
} finally {
state.reset()
}
}
override fun afterAll(context: ExtensionContext) {
val store = context.getStore(namespace)
store.remove(ApolloTestState::class)
}
override fun afterEach(context: ExtensionContext) = cleanup(context)
override fun afterTestExecution(context: ExtensionContext) = cleanup(context)
override fun beforeAll(context: ExtensionContext) {
val stubHandlers = MessageHandlerChainSet()
val stubWorld = spyk(World())
val pluginEnvironment = KotlinPluginEnvironment(stubWorld)
pluginEnvironment.setContext(FakePluginContextFactory.create(stubHandlers))
pluginEnvironment.load(ArrayList<PluginMetaData>())
val state = ApolloTestState(stubHandlers, stubWorld)
val store = context.getStore(namespace)
store.put(ApolloTestState::class, state)
}
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 store = context.getStore(namespace)
val state = store.get(ApolloTestState::class) as ApolloTestState
val propertyStubSites = testClassProps.asSequence()
.mapNotNull { it as? KMutableProperty<*> }
.filter { supportedTestDoubleTypes.contains(it.returnType) }
propertyStubSites.forEach {
it.setter.call(
testClassInstance,
state.createStub(StubPrototype(it.returnType.jvmErasure, it.annotations))
)
}
}
override fun supportsParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext): Boolean {
val param = parameterContext.parameter
val paramType = param.type.kotlin
return supportedTestDoubleTypes.contains(paramType.createType())
}
override fun resolveParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext): Any {
val param = parameterContext.parameter
val paramType = param.type.kotlin
val testStore = extensionContext.getStore(namespace)
val testState = testStore.get(ApolloTestState::class) as ApolloTestState
return testState.createStub(StubPrototype(paramType, param.annotations.toList()))
}
}
@@ -0,0 +1,72 @@
package org.apollo.game.plugin.testing.junit
import io.mockk.every
import io.mockk.slot
import io.mockk.spyk
import org.apollo.game.action.Action
import org.apollo.game.message.handler.MessageHandlerChainSet
import org.apollo.game.model.World
import org.apollo.game.model.entity.Player
import org.apollo.game.plugin.testing.junit.api.ActionCapture
import org.apollo.game.plugin.testing.junit.mocking.StubPrototype
import org.apollo.game.plugin.testing.junit.stubs.PlayerStubInfo
import org.apollo.net.message.Message
import org.apollo.util.security.PlayerCredentials
import kotlin.reflect.KClass
data class ApolloTestState(val handlers: MessageHandlerChainSet, val world: World) {
val players = mutableListOf<Player>()
var actionCapture: ActionCapture? = null
fun createActionCapture(type: KClass<out Action<*>>): ActionCapture {
if (actionCapture != null) {
throw IllegalStateException("Cannot specify more than one ActionCapture")
}
actionCapture = ActionCapture(type)
return actionCapture!!
}
fun <T : Any> createStub(proto: StubPrototype<T>): T {
val annotations = proto.annotations
return when (proto.type) {
Player::class -> createPlayer(PlayerStubInfo.create(annotations)) as T
World::class -> world as T
ActionCapture::class -> createActionCapture(Action::class) as T
else -> throw IllegalArgumentException("Can't stub ${proto.type.qualifiedName}")
}
}
fun createPlayer(info: PlayerStubInfo): Player {
val credentials = PlayerCredentials(info.name, "test", 1, 1, "0.0.0.0")
val region = world.regionRepository.fromPosition(info.position)
val player = spyk(Player(world, credentials, info.position))
world.register(player)
region.addEntity(player)
players.add(player)
val actionSlot = slot<Action<*>>()
val messageSlot = slot<Message>()
every { player.send(capture(messageSlot)) } answers { handlers.notify(player, messageSlot.captured) }
every { player.startAction(capture(actionSlot)) } answers {
actionCapture?.capture(actionSlot.captured)
true
}
return player
}
fun reset() {
actionCapture = null
players.forEach {
it.stopAction()
world.unregister(it)
}
players.clear()
}
}
@@ -0,0 +1,94 @@
package org.apollo.game.plugin.testing.junit.api
import org.apollo.game.action.Action
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import kotlin.reflect.KClass
import kotlin.reflect.full.isSuperclassOf
class ActionCapture(val type: KClass<out Action<*>>) {
private var action: Action<*>? = null
private val callbacks = mutableListOf<ActionCaptureCallback>()
private var lastTicks: Int = 0
fun capture(captured: Action<*>) {
assertTrue(type.isSuperclassOf(captured::class)) {
"${captured::class.simpleName} is not an instance of ${type.simpleName}"
}
this.action = captured
}
private fun callback(delay: ActionCaptureDelay): ActionCaptureCallbackRegistration {
val registration = ActionCaptureCallbackRegistration()
val callback = ActionCaptureCallback(delay, registration)
callbacks.add(callback)
return registration
}
fun runAction(timeout: Int = 50) {
action?.let {
var pulses = 0
do {
it.pulse()
pulses++
val tickCallbacks = callbacks.filter { it.delay == ActionCaptureDelay.Ticks(pulses) }
tickCallbacks.forEach { it.invoke() }
callbacks.removeAll(tickCallbacks)
} while (it.isRunning && pulses < timeout)
val completionCallbacks = callbacks.filter { it.delay == ActionCaptureDelay.Completed }
completionCallbacks.forEach { it.invoke() }
callbacks.removeAll(completionCallbacks)
}
assertEquals(0, callbacks.size, {
"untriggered callbacks:\n" + callbacks
.map {
val delayDescription = when (it.delay) {
is ActionCaptureDelay.Ticks -> "${it.delay.count} ticks"
is ActionCaptureDelay.Completed -> "action completion"
}
"$delayDescription (${it.callbackRegistration.description ?: ""})"
}
.joinToString("\n")
.prependIndent(" ")
})
}
/**
* Create a callback registration that triggers after exactly [count] ticks.
*/
fun exactTicks(count: Int) = callback(ActionCaptureDelay.Ticks(count))
/**
* Create a callback registration that triggers after [count] ticks. This method is cumulative,
* and will take into account previous calls to [ticks] when creating new callbacks.
*
* To run a callback after an exact number of ticks use [exactTicks].
*/
fun ticks(count: Int): ActionCaptureCallbackRegistration {
lastTicks += count
return exactTicks(lastTicks)
}
/**
* Create a callback registration that triggers when an [Action] completes.
*/
fun complete() = callback(ActionCaptureDelay.Completed)
/**
* Check if this capture has a pending [Action] to run.
*/
fun isPending(): Boolean {
return action?.isRunning ?: false
}
}
@@ -0,0 +1,7 @@
package org.apollo.game.plugin.testing.junit.api
data class ActionCaptureCallback(val delay: ActionCaptureDelay, val callbackRegistration: ActionCaptureCallbackRegistration) {
fun invoke() {
callbackRegistration.function?.invoke()
}
}
@@ -0,0 +1,5 @@
package org.apollo.game.plugin.testing.junit.api
typealias Function = () -> Unit
class ActionCaptureCallbackRegistration(var function: Function? = null, var description: String? = null)
@@ -0,0 +1,6 @@
package org.apollo.game.plugin.testing.junit.api
sealed class ActionCaptureDelay {
data class Ticks(val count: Int) : ActionCaptureDelay()
object Completed : ActionCaptureDelay()
}
@@ -0,0 +1,5 @@
package org.apollo.game.plugin.testing.junit.api.annotations
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class ItemDefinitions
@@ -0,0 +1,8 @@
package org.apollo.game.plugin.testing.junit.api.annotations
annotation class Id(val value: Int)
annotation class Pos(val x: Int, val y: Int, val height: Int = 0)
@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.RUNTIME)
annotation class TestMock
@@ -0,0 +1,6 @@
package org.apollo.game.plugin.testing.junit.api.annotations
import org.apollo.game.action.Action
import kotlin.reflect.KClass
annotation class ActionTest(val value: KClass<out Action<*>> = Action::class)
@@ -0,0 +1,42 @@
package org.apollo.game.plugin.testing.junit.api.interactions
import org.apollo.game.message.impl.ItemOptionMessage
import org.apollo.game.message.impl.NpcActionMessage
import org.apollo.game.message.impl.ObjectActionMessage
import org.apollo.game.message.impl.PlayerActionMessage
import org.apollo.game.model.Direction
import org.apollo.game.model.Position
import org.apollo.game.model.entity.Entity
import org.apollo.game.model.entity.Npc
import org.apollo.game.model.entity.Player
import org.apollo.game.model.entity.obj.GameObject
/**
* Send an [ItemOptionMessage] for the given [id], [option], [slot], and [interfaceId], simulating a
* player interacting with an item.
*/
fun Player.interactWithItem(id: Int, option: Int, slot: Int? = null, interfaceId: Int? = null) {
send(ItemOptionMessage(option, interfaceId ?: -1, id, slot ?: inventory.slotOf(id)))
}
/**
* Spawn a new object (defaulting to in-front of the player) and immediately interact with it.
*/
fun Player.interactWithObject(id: Int, option: Int, at: Position? = null) {
val obj = world.spawnObject(id, at ?: position.step(1, Direction.NORTH))
interactWith(obj, option)
}
/**
* Move the player within interaction distance to the given [Entity] and fake an action
* message.
*/
fun Player.interactWith(entity: Entity, option: Int = 1) {
position = entity.position.step(1, Direction.NORTH)
when (entity) {
is GameObject -> send(ObjectActionMessage(option, entity.id, entity.position))
is Npc -> send(NpcActionMessage(option, entity.index))
is Player -> send(PlayerActionMessage(option, entity.index))
}
}
@@ -0,0 +1,35 @@
package org.apollo.game.plugin.testing.junit.api.interactions
import org.apollo.cache.def.NpcDefinition
import org.apollo.game.model.Position
import org.apollo.game.model.World
import org.apollo.game.model.entity.Npc
import org.apollo.game.model.entity.obj.GameObject
import org.apollo.game.model.entity.obj.StaticGameObject
/**
* Spawn a new static game object into the world with the given id and position.
*/
fun World.spawnObject(id: Int, position: Position): GameObject {
val obj = StaticGameObject(this, id, position, 0, 0)
spawn(obj)
return obj
}
/**
* Spawns a new NPC with the minimum set of dependencies required to function correctly in the world.
*/
fun World.spawnNpc(id: Int, position: Position): Npc {
val definition = NpcDefinition(id)
val npc = Npc(this, position, definition, arrayOfNulls(4))
val region = regionRepository.fromPosition(position)
val npcs = npcRepository
npcs.add(npc)
region.addEntity(npc)
return npc
}
@@ -0,0 +1,5 @@
package org.apollo.game.plugin.testing.junit.mocking
import kotlin.reflect.KClass
data class StubPrototype<T : Any>(val type: KClass<T>, val annotations: Collection<Annotation>)
@@ -0,0 +1,2 @@
package org.apollo.game.plugin.testing.junit.stubs
@@ -0,0 +1,2 @@
package org.apollo.game.plugin.testing.junit.stubs
@@ -0,0 +1,25 @@
package org.apollo.game.plugin.testing.junit.stubs
import org.apollo.game.model.Position
import org.apollo.game.plugin.testing.junit.api.annotations.Pos
class PlayerStubInfo {
companion object {
fun create(annotations: Collection<Annotation>): PlayerStubInfo {
val info = PlayerStubInfo()
annotations.forEach {
when (it) {
is Pos -> info.position = Position(it.x, it.y, it.height)
}
}
return info
}
}
var position = Position(3222, 3222)
var name = "test"
}
@@ -1,25 +0,0 @@
package org.apollo.game.plugin.testing.mockito
import org.mockito.ArgumentMatcher
import java.lang.AssertionError
import java.util.function.Consumer
class KotlinArgMatcher<T>(val consumer: Consumer<T>) : ArgumentMatcher<T>() {
private var error: String? = null
override fun matches(argument: Any?): Boolean {
try {
consumer.accept(argument as T)
return true
} catch (err: AssertionError) {
error = err.message
println(error)
return false
}
}
override fun toString(): String {
return error ?: ""
}
}
@@ -1,14 +0,0 @@
package org.apollo.game.plugin.testing.mockito
import org.mockito.Mockito
import java.util.function.Consumer
object KotlinMockitoExtensions {
inline fun <reified T> matches(crossinline callback: T.() -> Unit): T {
val consumer = Consumer<T> { it.callback() }
val matcher = KotlinArgMatcher(consumer)
return Mockito.argThat(matcher)
}
}
+11 -13
View File
@@ -29,30 +29,28 @@ val Player.runecraft: SkillProxy get() = SkillProxy(skillSet, Skill.RUNECRAFT)
/**
* A proxy class to allow
*/
class SkillProxy(val skills: SkillSet, val skill: Int) {
class SkillProxy(private val skills: SkillSet, private val skill: Int) {
/**
* The maximum level of this skill.
*/
val maximum = skills.getMaximumLevel(skill)
val maximum: Int
get() = skills.getMaximumLevel(skill)
/**
* The current level of this skill.
*/
val current = skills.getCurrentLevel(skill)
val experience = ExperienceProxy()
val current: Int
get() = skills.getCurrentLevel(skill)
/**
* A proxy class to make [experience] (effectively) write-only.
* The amount of experience in this skill a player has.
*/
inner class ExperienceProxy {
operator fun plusAssign(amount: Int) = skills.addExperience(skill, amount.toDouble())
operator fun plusAssign(amount: Double) = skills.addExperience(skill, amount)
}
var experience: Double
get() = skills.getExperience(skill)
set(value) {
skills.setExperience(skill, value)
}
/**
* Boosts the current level of this skill by [amount], if possible (i.e. if `current + amount <= maximum + amount`).
+2 -2
View File
@@ -1,8 +1,8 @@
package org.apollo.game.plugin.api
import java.util.Random
import java.util.*
val RAND = Random()
public val RAND = Random()
fun rand(bounds: Int): Int {
return RAND.nextInt(bounds)
-17
View File
@@ -1,17 +0,0 @@
import org.apollo.cache.def.ItemDefinition
import org.apollo.game.plugin.api.Definitions
import org.apollo.game.plugin.testing.KotlinPluginTest
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
class NamedLookupTests : KotlinPluginTest() {
@Test
fun itemLookup() {
val testItem = ItemDefinition(0)
testItem.name = "sword"
ItemDefinition.init(arrayOf(testItem))
assertThat(Definitions.item("sword")).isEqualTo(testItem)
}
}
+26 -9
View File
@@ -1,9 +1,19 @@
import org.apollo.game.model.Position
import org.apollo.game.plugin.testing.KotlinPluginTest
import org.junit.Test
import org.mockito.Mockito.verify
class OpenBankTest() : KotlinPluginTest() {
import org.apollo.game.model.Position
import org.apollo.game.model.World
import org.apollo.game.model.entity.Player
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.TestMock
import org.apollo.game.plugin.testing.junit.api.interactions.interactWith
import org.apollo.game.plugin.testing.junit.api.interactions.spawnNpc
import org.apollo.game.plugin.testing.junit.api.interactions.spawnObject
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
@ExtendWith(ApolloTestingExtension::class)
class OpenBankTest {
companion object {
const val BANK_BOOTH_ID = 2213
@@ -12,15 +22,23 @@ class OpenBankTest() : KotlinPluginTest() {
val BANK_POSITION = Position(3200, 3200, 0)
}
@TestMock
lateinit var action: ActionCapture
@TestMock
lateinit var player: Player
@TestMock
lateinit var world: World
@Test
fun `Interacting with a bank teller should open the players bank`() {
val bankTeller = world.spawnNpc(BANK_TELLER_ID, BANK_POSITION)
// @todo - these option numbers only match by coincidence, we should be looking up the correct ones
player.interactWith(bankTeller, option = 2)
player.waitForActionCompletion()
verify(player).openBank()
verifyAfter(action.complete()) { player.openBank() }
}
@Test
@@ -28,9 +46,8 @@ class OpenBankTest() : KotlinPluginTest() {
val bankBooth = world.spawnObject(BANK_BOOTH_ID, BANK_POSITION)
player.interactWith(bankBooth, option = 2)
player.waitForActionCompletion()
verify(player).openBank()
verifyAfter(action.complete()) { player.openBank() }
}
}
+15
View File
@@ -1,6 +1,21 @@
buildscript {
repositories {
jcenter()
mavenCentral()
maven { url "https://plugins.gradle.org/m2/" }
}
dependencies {
classpath 'org.junit.platform:junit-platform-gradle-plugin:1.1.0'
}
}
subprojects { subproj ->
if (subproj.buildFile.exists()) {
apply plugin: 'apollo-plugin'
apply plugin: 'org.junit.platform.gradle.plugin'
dependencies {
implementation group: 'com.google.guava', name: 'guava', version: guavaVersion
@@ -1,16 +1,24 @@
package org.apollo.plugin.consumables
import org.apollo.game.message.impl.ItemOptionMessage
import io.mockk.verify
import org.apollo.game.model.entity.Player
import org.apollo.game.model.entity.Skill
import org.apollo.game.plugin.testing.KotlinPluginTest
import org.apollo.game.plugin.testing.mockito.KotlinMockitoExtensions.matches
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Test
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.apollo.game.plugin.testing.assertions.contains
import org.apollo.game.plugin.testing.assertions.after
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.TestMock
import org.apollo.game.plugin.testing.junit.api.interactions.interactWithItem
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.extension.ExtendWith
class FoodOrDrinkTests : KotlinPluginTest() {
@ExtendWith(ApolloTestingExtension::class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class FoodOrDrinkTests {
companion object {
const val TEST_FOOD_NAME = "test_food"
@@ -25,9 +33,14 @@ class FoodOrDrinkTests : KotlinPluginTest() {
const val MAX_HP_LEVEL = 10
}
@Before override fun setup() {
super.setup()
@TestMock
lateinit var player: Player
@TestMock
lateinit var action: ActionCapture
@BeforeEach
fun setup() {
val skills = player.skillSet
skills.setCurrentLevel(Skill.HITPOINTS, HP_LEVEL)
skills.setMaximumLevel(Skill.HITPOINTS, MAX_HP_LEVEL)
@@ -36,51 +49,47 @@ class FoodOrDrinkTests : KotlinPluginTest() {
drink("test_drink", TEST_DRINK_ID, TEST_DRINK_RESTORATION)
}
@Test fun `Consuming food or drink should restore the players hitpoints`() {
@Test
fun `Consuming food or drink should restore the players hitpoints`() {
val expectedHpLevel = TEST_FOOD_RESTORATION + HP_LEVEL
player.notify(ItemOptionMessage(1, -1, TEST_FOOD_ID, 1))
player.waitForActionCompletion()
player.interactWithItem(TEST_FOOD_ID, option = 1, slot = 1)
val currentHpLevel = player.skillSet.getCurrentLevel(Skill.HITPOINTS)
assertThat(currentHpLevel).isEqualTo(expectedHpLevel)
after(action.complete()) {
assertEquals(expectedHpLevel, player.skillSet.getCurrentLevel(Skill.HITPOINTS))
}
}
@Test fun `A message should be sent notifying the player if the item restored hitpoints`() {
player.notify(ItemOptionMessage(1, -1, TEST_FOOD_ID, 1))
player.waitForActionCompletion()
@Test
fun `A message should be sent notifying the player if the item restored hitpoints`() {
player.interactWithItem(TEST_FOOD_ID, option = 1, slot = 1)
verify(player).sendMessage(matches {
assertThat(this).contains("heals some health")
})
verifyAfter(action.complete()) { player.sendMessage("It heals some health.") }
}
@Test fun `A message should not be sent to the player if the item did not restore hitpoints`() {
@Test
fun `A message should not be sent to the player if the item did not restore hitpoints`() {
player.skillSet.setCurrentLevel(Skill.HITPOINTS, MAX_HP_LEVEL)
player.notify(ItemOptionMessage(1, -1, TEST_FOOD_ID, 1))
player.waitForActionCompletion()
player.interactWithItem(TEST_FOOD_ID, option = 1, slot = 1)
verify(player, never()).sendMessage(matches {
assertThat(this).contains("heals some health")
})
after(action.complete()) {
verify(exactly = 0) { player.sendMessage(contains("it heals some")) }
}
}
@Test fun `A message should be sent saying the player has drank an item when consuming a drink`() {
player.notify(ItemOptionMessage(1, -1, TEST_DRINK_ID, 1))
player.waitForActionCompletion()
@Test
fun `A message should be sent saying the player has drank an item when consuming a drink`() {
player.interactWithItem(TEST_DRINK_ID, option = 1, slot = 1)
verifyAfter(action.complete()) { player.sendMessage("You drink the ${TEST_DRINK_NAME}.") }
verify(player).sendMessage(matches {
assertThat(this).contains("You drink the ${TEST_DRINK_NAME}")
})
}
@Test fun `A message should be sent saying the player has eaten an item when consuming food`() {
player.notify(ItemOptionMessage(1, -1, TEST_FOOD_ID, 1))
player.waitForActionCompletion()
@Test
fun `A message should be sent saying the player has eaten an item when consuming food`() {
player.interactWithItem(TEST_FOOD_ID, option = 1, slot = 1)
verify(player).sendMessage(matches {
assertThat(this).contains("You eat the ${TEST_FOOD_NAME}")
})
verifyAfter(action.complete()) { player.sendMessage("You eat the ${TEST_FOOD_NAME}.") }
}
}
+39 -18
View File
@@ -1,43 +1,64 @@
import org.apollo.game.model.Position
import org.apollo.game.model.entity.Skill
import org.apollo.game.plugin.testing.*
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import org.mockito.Mockito.contains
import org.mockito.Mockito.verify
class TrainingDummyTest : KotlinPluginTest() {
import io.mockk.verify
import org.apollo.game.model.Position
import org.apollo.game.model.World
import org.apollo.game.model.entity.Player
import org.apollo.game.model.entity.Skill
import org.apollo.game.plugin.testing.assertions.after
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.TestMock
import org.apollo.game.plugin.testing.junit.api.interactions.interactWith
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
import org.junit.jupiter.api.extension.ExtendWith
@ExtendWith(ApolloTestingExtension::class)
class TrainingDummyTest {
companion object {
const val DUMMY_ID = 823
val DUMMY_POSITION = Position(3200, 3230, 0)
}
@Test fun `Hitting the training dummy should give the player attack experience`() {
@TestMock
lateinit var action: ActionCapture
@TestMock
lateinit var player: Player
@TestMock
lateinit var world: World
@Test
fun `Hitting the training dummy should give the player attack experience`() {
val dummy = world.spawnObject(DUMMY_ID, DUMMY_POSITION)
val skills = player.skillSet
val beforeExp = skills.getExperience(Skill.ATTACK)
player.interactWith(dummy, option = 2)
player.waitForActionCompletion()
val afterExp = skills.getExperience(Skill.ATTACK)
assertThat(afterExp).isGreaterThan(beforeExp)
after(action.complete()) {
assertTrue(skills.getExperience(Skill.ATTACK) > beforeExp)
}
}
@Test fun `The player should stop getting attack experience from the training dummy at level 8`() {
@Test
fun `The player should stop getting attack experience from the training dummy at level 8`() {
val dummy = world.spawnObject(DUMMY_ID, DUMMY_POSITION)
val skills = player.skillSet
skills.setMaximumLevel(Skill.ATTACK, 8)
val beforeExp = skills.getExperience(Skill.ATTACK)
player.interactWith(dummy, option = 2)
player.waitForActionCompletion()
val afterExp = skills.getExperience(Skill.ATTACK)
verify(player).sendMessage(contains("nothing more you can learn"))
assertThat(afterExp).isEqualTo(beforeExp)
after(action.complete()) {
verify { player.sendMessage(contains("nothing more you can learn")) }
assertEquals(beforeExp, skills.getExperience(Skill.ATTACK))
}
}
}
+17 -9
View File
@@ -1,19 +1,27 @@
import org.apollo.game.message.impl.ButtonMessage
import org.apollo.game.plugin.testing.KotlinPluginTest
import org.junit.Test
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
class LogoutTests : KotlinPluginTest() {
import io.mockk.verify
import org.apollo.game.message.impl.ButtonMessage
import org.apollo.game.model.entity.Player
import org.apollo.game.plugin.testing.junit.ApolloTestingExtension
import org.apollo.game.plugin.testing.junit.api.annotations.TestMock
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
@ExtendWith(ApolloTestingExtension::class)
class LogoutTests {
companion object {
const val LOGOUT_BUTTON_ID = 2458
}
@Test fun `The player should be logged out when they click the logout button`() {
player.notify(ButtonMessage(LOGOUT_BUTTON_ID))
@TestMock
lateinit var player: Player
verify(player, times(1)).logout()
@Test
fun `The player should be logged out when they click the logout button`() {
player.send(ButtonMessage(LOGOUT_BUTTON_ID))
verify { player.logout() }
}
}
+17 -16
View File
@@ -9,12 +9,12 @@ import org.apollo.game.plugin.skills.fishing.FishingTool.*
/**
* A fish that can be gathered using the fishing skill.
*/
enum class Fish(val id: Int, val level: Int, val experience: Double) {
SHRIMP(id = 317, level = 1, experience = 10.0),
enum class Fish(val id: Int, val level: Int, val experience: Double, catchSuffix: String? = null) {
SHRIMPS(id = 317, level = 1, experience = 10.0, catchSuffix = "some shrimp."),
SARDINE(id = 327, level = 5, experience = 20.0),
MACKEREL(id = 353, level = 16, experience = 20.0),
HERRING(id = 345, level = 10, experience = 30.0),
ANCHOVY(id = 321, level = 15, experience = 40.0),
ANCHOVIES(id = 321, level = 15, experience = 40.0, catchSuffix = "some anchovies."),
TROUT(id = 335, level = 20, experience = 50.0),
COD(id = 341, level = 23, experience = 45.0),
PIKE(id = 349, level = 25, experience = 60.0),
@@ -23,13 +23,14 @@ enum class Fish(val id: Int, val level: Int, val experience: Double) {
LOBSTER(id = 377, level = 40, experience = 90.0),
BASS(id = 363, level = 46, experience = 100.0),
SWORDFISH(id = 371, level = 50, experience = 100.0),
SHARK(id = 383, level = 76, experience = 110.0);
SHARK(id = 383, level = 76, experience = 110.0, catchSuffix = "a shark!");
/**
* The name of this fish, formatted so it can be inserted into a message.
*/
val formattedName = Definitions.item(id)!!.name.toLowerCase()
// TODO this leads to incorrect messages, e.g. 'You catch a raw shrimps'.
val catchMessage = "You catch ${catchSuffix ?: "a ${catchName()}."}"
private fun catchName() = Definitions.item(id)!!.name.toLowerCase().removePrefix("raw ")
}
@@ -69,16 +70,7 @@ enum class FishingSpot(val npc: Int, private val first: Option, private val seco
ROD(309, Option.of(FLY_FISHING_ROD, TROUT, SALMON), Option.of(FISHING_ROD, PIKE)),
CAGE_HARPOON(312, Option.of(LOBSTER_CAGE, LOBSTER), Option.of(HARPOON, TUNA, SWORDFISH)),
NET_HARPOON(313, Option.of(BIG_NET, MACKEREL, COD), Option.of(HARPOON, BASS, SHARK)),
NET_ROD(316, Option.of(SMALL_NET, SHRIMP, ANCHOVY), Option.of(FISHING_ROD, SARDINE, HERRING));
companion object {
private val FISHING_SPOTS = FishingSpot.values().associateBy({ it.npc }, { it })
/**
* Returns the [FishingSpot] with the specified [id], or `null` if the spot does not exist.
*/
fun lookup(id: Int): FishingSpot? = FISHING_SPOTS[id]
}
NET_ROD(316, Option.of(SMALL_NET, SHRIMPS, ANCHOVIES), Option.of(FISHING_ROD, SARDINE, HERRING));
/**
* Returns the [FishingSpot.Option] associated with the specified action id.
@@ -159,4 +151,13 @@ enum class FishingSpot(val npc: Int, private val first: Option, private val seco
}
companion object {
private val FISHING_SPOTS = FishingSpot.values().associateBy(FishingSpot::npc)
/**
* Returns the [FishingSpot] with the specified [id], or `null` if the spot does not exist.
*/
fun lookup(id: Int): FishingSpot? = FISHING_SPOTS[id]
}
}
@@ -57,7 +57,7 @@ class FishingAction(player: Player, position: Position, val option: FishingSpot.
}
mob.inventory.add(fish.id)
mob.sendMessage("You catch a ${fish.formattedName}.")
mob.sendMessage(fish.catchMessage)
mob.fishing.experience += fish.experience
if (mob.inventory.freeSlots() == 0) {
+1 -1
View File
@@ -10,5 +10,5 @@ plugin {
"tlf30"
]
dependencies = ["api"]
dependencies = ["api", "util:lookup"]
}
+1 -1
View File
@@ -8,6 +8,6 @@ enum class Gem(val id: Int) { // TODO add gem drop chances
companion object {
private val GEMS = Gem.values().associateBy({ it.id }, { it })
fun lookup(id: Int): Gem? = GEMS[id]
operator fun get(id: Int): Gem? = GEMS[id]
}
}
+146
View File
@@ -0,0 +1,146 @@
import org.apollo.game.action.ActionBlock
import org.apollo.game.action.AsyncDistancedAction
import org.apollo.game.message.impl.ObjectActionMessage
import org.apollo.game.model.Position
import org.apollo.game.model.World
import org.apollo.game.model.entity.Player
import org.apollo.game.model.entity.obj.GameObject
import org.apollo.game.plugin.api.*
import org.apollo.game.plugin.skills.mining.Ore
import org.apollo.game.plugin.skills.mining.Pickaxe
import org.apollo.net.message.Message
import java.util.*
class MiningAction(
player: Player,
private val tool: Pickaxe,
private val target: MiningTarget
) : AsyncDistancedAction<Player>(PULSES, true, player, target.position, ORE_SIZE) {
companion object {
private const val PULSES = 0
private const val ORE_SIZE = 1
/**
* Starts a [MiningAction] for the specified [Player], terminating the [Message] that triggered it.
*/
fun start(message: ObjectActionMessage, player: Player, ore: Ore) {
val pickaxe = Pickaxe.bestFor(player)
if (pickaxe == null) {
player.sendMessage("You do not have a pickaxe for which you have the level to use.")
} else {
val target = MiningTarget(message.id, message.position, ore)
val action = MiningAction(player, pickaxe, target)
player.startAction(action)
}
message.terminate()
}
}
override fun action(): ActionBlock = {
mob.turnTo(position)
if (!target.skillRequirementsMet(mob)) {
mob.sendMessage("You do not have the required level to mine this rock.")
stop()
}
mob.sendMessage("You swing your pick at the rock.")
mob.playAnimation(tool.animation)
wait(tool.pulses)
if (!target.isValid(mob.world)) {
stop()
}
val successChance = rand(100)
if (target.isSuccessful(mob, successChance)) {
if (mob.inventory.freeSlots() == 0) {
mob.inventory.forceCapacityExceeded()
stop()
}
if (target.reward(mob)) {
mob.sendMessage("You manage to mine some ${target.oreName()}")
target.deplete(mob.world)
stop()
}
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as MiningAction
return mob == other.mob && target == other.target
}
override fun hashCode(): Int = Objects.hash(mob, target)
}
data class MiningTarget(val objectId: Int, val position: Position, val ore: Ore) {
/**
* Get the [GameObject] represented by this target.
*
* @todo: api: shouldn't be as verbose
*/
private fun getObject(world: World): GameObject? {
val region = world.regionRepository.fromPosition(position)
return region.findObject(position, objectId).orElse(null)
}
/**
* Deplete this mining resource from the [World], and schedule it to be respawned
* in a number of ticks specified by the [Ore].
*/
fun deplete(world: World) {
world.expireObject(getObject(world)!!, ore.objects[objectId]!!, ore.respawn)
}
/**
* Check if the [Player] was successful in mining this ore with a random success [chance] value between 0 and 100.
*/
fun isSuccessful(mob: Player, chance: Int): Boolean {
val percent = (ore.chance * mob.mining.current + ore.chanceOffset) * 100
return chance < percent
}
/**
* Check if this target is still valid in the [World] (i.e. has not been [deplete]d).
*/
fun isValid(world: World) = getObject(world) != null
/**
* Get the normalized name of the [Ore] represented by this target.
*/
fun oreName() = Definitions.item(ore.id)!!.name.toLowerCase()
/**
* Reward a [player] with experience and ore if they have the inventory capacity to take a new ore.
*/
fun reward(player: Player): Boolean {
val hasInventorySpace = player.inventory.add(ore.id)
if (hasInventorySpace) {
player.mining.experience += ore.exp
}
return hasInventorySpace
}
/**
* Check if the [mob] has met the skill requirements to mine te [Ore] represented by
* this [MiningTarget].
*/
fun skillRequirementsMet(mob: Player) = mob.mining.current < ore.level
}
@@ -1,19 +1,5 @@
import org.apollo.game.action.ActionBlock
import org.apollo.game.action.AsyncDistancedAction
import org.apollo.game.message.impl.ObjectActionMessage
import org.apollo.game.model.Position
import org.apollo.game.model.World
import org.apollo.game.model.entity.Player
import org.apollo.game.model.entity.obj.GameObject
import org.apollo.game.plugin.api.Definitions
import org.apollo.game.plugin.api.expireObject
import org.apollo.game.plugin.api.findObject
import org.apollo.game.plugin.api.mining
import org.apollo.game.plugin.api.rand
import org.apollo.game.plugin.skills.mining.Ore
import org.apollo.game.plugin.skills.mining.Pickaxe
import org.apollo.net.message.Message
import java.util.Objects
on { ObjectActionMessage::class }
.where { option == Actions.MINING }
@@ -35,99 +21,6 @@ on { ObjectActionMessage::class }
}
}
class MiningAction(
player: Player,
private val tool: Pickaxe,
private val target: MiningTarget
) : AsyncDistancedAction<Player>(PULSES, true, player, target.position, ORE_SIZE) {
companion object {
private const val PULSES = 0
private const val ORE_SIZE = 1
/**
* Starts a [MiningAction] for the specified [Player], terminating the [Message] that triggered it.
*/
fun start(message: ObjectActionMessage, player: Player, ore: Ore) {
val pickaxe = Pickaxe.bestFor(player)
if (pickaxe == null) {
player.sendMessage("You do not have a pickaxe for which you have the level to use.")
} else {
val target = MiningTarget(message.id, message.position, ore)
val action = MiningAction(player, pickaxe, target)
player.startAction(action)
}
message.terminate()
}
}
override fun action(): ActionBlock = {
mob.turnTo(position)
val level = mob.mining.current
if (level < target.ore.level) {
mob.sendMessage("You do not have the required level to mine this rock.")
stop()
}
mob.sendMessage("You swing your pick at the rock.")
mob.playAnimation(tool.animation)
wait(tool.pulses)
val obj = target.getObject(mob.world)
if (obj == null) {
stop()
}
if (target.isSuccessful(mob)) {
if (mob.inventory.freeSlots() == 0) {
mob.inventory.forceCapacityExceeded()
stop()
}
if (mob.inventory.add(target.ore.id)) {
val oreName = Definitions.item(target.ore.id)!!.name.toLowerCase()
mob.sendMessage("You manage to mine some $oreName")
mob.mining.experience += target.ore.exp
mob.world.expireObject(obj!!, target.ore.objects[target.objectId]!!, target.ore.respawn)
stop()
}
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as MiningAction
return mob == other.mob && target == other.target
}
override fun hashCode(): Int = Objects.hash(mob, target)
}
data class MiningTarget(val objectId: Int, val position: Position, val ore: Ore) {
fun getObject(world: World): GameObject? {
val region = world.regionRepository.fromPosition(position)
return region.findObject(position, objectId).orElse(null)
}
fun isSuccessful(mob: Player): Boolean {
val offset = if (ore.chanceOffset) 1 else 0
val percent = (ore.chance * mob.mining.current + offset) * 100
return rand(100) < percent
}
}
private object Actions {
const val MINING = 1
const val PROSPECTING = 2
+5 -5
View File
@@ -17,12 +17,12 @@ enum class Ore(
val exp: Double,
val respawn: Int,
val chance: Double,
val chanceOffset: Boolean = false
val chanceOffset: Double = 0.0
) {
CLAY(CLAY_OBJECTS, id = 434, level = 1, exp = 5.0, respawn = 1, chance = 0.0085, chanceOffset = true),
COPPER(COPPER_OBJECTS, id = 436, level = 1, exp = 17.5, respawn = 4, chance = 0.0085, chanceOffset = true),
TIN(TIN_OBJECTS, id = 438, level = 1, exp = 17.5, respawn = 4, chance = 0.0085, chanceOffset = true),
IRON(IRON_OBJECTS, id = 440, level = 15, exp = 35.0, respawn = 9, chance = 0.0085, chanceOffset = true),
CLAY(CLAY_OBJECTS, id = 434, level = 1, exp = 5.0, respawn = 1, chance = 0.0085, chanceOffset = 0.45),
COPPER(COPPER_OBJECTS, id = 436, level = 1, exp = 17.5, respawn = 4, chance = 0.0085, chanceOffset = 0.45),
TIN(TIN_OBJECTS, id = 438, level = 1, exp = 17.5, respawn = 4, chance = 0.0085, chanceOffset = 0.45),
IRON(IRON_OBJECTS, id = 440, level = 15, exp = 35.0, respawn = 9, chance = 0.0085, chanceOffset = 0.45),
COAL(COAL_OBJECTS, id = 453, level = 30, exp = 50.0, respawn = 50, chance = 0.004),
GOLD(GOLD_OBJECTS, id = 444, level = 40, exp = 65.0, respawn = 100, chance = 0.003),
SILVER(SILVER_OBJECTS, id = 442, level = 20, exp = 40.0, respawn = 100, chance = 0.0085),
+1 -1
View File
@@ -7,7 +7,7 @@ import org.apollo.game.plugin.api.mining
enum class Pickaxe(val id: Int, val level: Int, animation: Int, val pulses: Int) {
BRONZE(id = 1265, level = 1, animation = 625, pulses = 8),
ITRON(id = 1267, level = 1, animation = 626, pulses = 7),
STEEL(id = 1269, level = 1, animation = 627, pulses = 6),
STEEL(id = 1269, level = 6, animation = 627, pulses = 6),
MITHRIL(id = 1273, level = 21, animation = 629, pulses = 5),
ADAMANT(id = 1271, level = 31, animation = 628, pulses = 4),
RUNE(id = 1275, level = 41, animation = 624, pulses = 3);
@@ -0,0 +1,80 @@
import io.mockk.every
import io.mockk.spyk
import io.mockk.staticMockk
import io.mockk.verify
import org.apollo.cache.def.ItemDefinition
import org.apollo.game.model.World
import org.apollo.game.model.entity.Player
import org.apollo.game.model.entity.Skill
import org.apollo.game.plugin.api.expireObject
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.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
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
@TestMock
lateinit var player: Player
@TestMock
lateinit var action: ActionCapture
@Test
fun `Attempting to mine a rock we don't have the skill to should send the player a message`() {
val obj = world.spawnObject(1, player.position)
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")) }
}
@Test
fun `Mining a rock we have the skill to mine should eventually reward ore and experience`() {
val (tinId, expiredTinId) = TIN_OBJ_IDS
val obj = world.spawnObject(tinId, player.position)
val target = spyk(MiningTarget(obj.id, obj.position, Ore.TIN))
staticMockk("org.apollo.game.plugin.api.WorldKt").mock()
every { target.skillRequirementsMet(player) } returns true
every { target.isSuccessful(player, any()) } returns true
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")) }
after(action.complete()) {
verify { player.sendMessage("You manage to mine some <ore_type>") }
verify { world.expireObject(obj, expiredTinId, Ore.TIN.respawn) }
assertTrue(player.inventory.contains(Ore.TIN.id))
assertEquals(player.skillSet.getExperience(Skill.MINING), Ore.TIN.exp)
}
}
}
@@ -0,0 +1,75 @@
import org.apollo.cache.def.ItemDefinition
import org.apollo.game.model.entity.Player
import org.apollo.game.model.entity.Skill
import org.apollo.game.plugin.skills.mining.Pickaxe
import org.apollo.game.plugin.testing.junit.ApolloTestingExtension
import org.apollo.game.plugin.testing.junit.api.annotations.ItemDefinitions
import org.apollo.game.plugin.testing.junit.api.annotations.TestMock
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.extension.ExtendWith
import org.junit.jupiter.params.ParameterizedTest
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
@ParameterizedTest
@EnumSource(Pickaxe::class)
fun `No pickaxe is chosen if none are available`(pickaxe: Pickaxe) {
player.skillSet.setCurrentLevel(Skill.MINING, pickaxe.level)
assertEquals(null, Pickaxe.bestFor(player))
}
@ParameterizedTest
@EnumSource(Pickaxe::class)
fun `The highest level pickaxe is chosen when available`(pickaxe: Pickaxe) {
player.skillSet.setCurrentLevel(Skill.MINING, pickaxe.level)
player.inventory.add(pickaxe.id)
assertEquals(pickaxe, Pickaxe.bestFor(player))
}
@ParameterizedTest
@EnumSource(Pickaxe::class)
fun `Only pickaxes the player has are chosen`(pickaxe: Pickaxe) {
player.skillSet.setCurrentLevel(Skill.MINING, pickaxe.level)
player.inventory.add(Pickaxe.BRONZE.id)
assertEquals(Pickaxe.BRONZE, Pickaxe.bestFor(player))
}
@ParameterizedTest
@EnumSource(value = Pickaxe::class)
fun `Pickaxes can be chosen from equipment as well as inventory`(pickaxe: Pickaxe) {
player.skillSet.setCurrentLevel(Skill.MINING, pickaxe.level)
player.inventory.add(pickaxe.id)
assertEquals(pickaxe, Pickaxe.bestFor(player))
}
@ParameterizedTest
@EnumSource(value = Pickaxe::class)
fun `Pickaxes with a level requirement higher than the player's are ignored`(pickaxe: Pickaxe) {
player.skillSet.setCurrentLevel(Skill.MINING, pickaxe.level)
player.inventory.add(pickaxe.id)
Pickaxe.values()
.filter { it.level > pickaxe.level }
.forEach { player.inventory.add(it.id) }
assertEquals(pickaxe, Pickaxe.bestFor(player))
}
}
@@ -0,0 +1,46 @@
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.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
@ExtendWith(ApolloTestingExtension::class)
class ProspectingTests {
@ItemDefinitions
fun ores() = Ore.values().map {
ItemDefinition(it.id).also { it.name = "<ore_type>" }
}
@TestMock
lateinit var player: Player
@TestMock
lateinit var action: ActionCapture
@ParameterizedTest
@ArgumentsSource(MiningTestDataProvider::class)
fun `Prospecting a rock should reveal the type of ore it contains`(data: MiningTestData) {
player.interactWithObject(data.rockId, 2)
verifyAfter(action.ticks(1)) { player.sendMessage(contains("examine the rock")) }
verifyAfter(action.complete()) { player.sendMessage(contains("This rock contains <ore_type>")) }
}
@ParameterizedTest
@ArgumentsSource(MiningTestDataProvider::class)
fun `Prospecting an expired rock should reveal it contains no ore`(data: MiningTestData) {
player.interactWithObject(data.expiredRockId, 2)
verifyAfter(action.complete()) { player.sendMessage(contains("no ore available in this rock")) }
}
}
@@ -0,0 +1,17 @@
import org.apollo.game.plugin.skills.mining.Ore
import org.junit.jupiter.api.extension.ExtensionContext
import org.junit.jupiter.params.provider.Arguments
import org.junit.jupiter.params.provider.ArgumentsProvider
import java.util.stream.Stream
data class MiningTestData(val rockId: Int, val expiredRockId: Int, val ore: Ore)
fun miningTestData(): Collection<MiningTestData> = Ore.values()
.flatMap { ore -> ore.objects.map { MiningTestData(it.key, it.value, ore) } }
.toList()
class MiningTestDataProvider : ArgumentsProvider {
override fun provideArguments(context: ExtensionContext?): Stream<out Arguments> {
return miningTestData().map { Arguments { arrayOf(it) } }.stream()
}
}
@@ -25,7 +25,7 @@ class BuryBoneAction(
}
companion object {
private val BURY_BONE_ANIMATION = Animation(827)
public val BURY_BONE_ANIMATION = Animation(827)
internal const val BURY_OPTION = 1
}
@@ -34,4 +34,4 @@ on { ItemOptionMessage::class }
player.startAction(BuryBoneAction(player, slot, bone))
terminate()
}
}
@@ -0,0 +1,72 @@
import BuryBoneAction.Companion.BURY_BONE_ANIMATION
import io.mockk.verify
import org.apollo.cache.def.ItemDefinition
import org.apollo.game.model.entity.Player
import org.apollo.game.plugin.api.prayer
import org.apollo.game.plugin.testing.assertions.after
import org.apollo.game.plugin.testing.assertions.startsWith
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.interactWithItem
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.extension.ExtendWith
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.EnumSource
@ExtendWith(ApolloTestingExtension::class)
class BuryBoneTests {
@TestMock
lateinit var player: Player
@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) {
player.inventory.add(bone.id)
player.interactWithItem(bone.id, option = 1)
verifyAfter(action.ticks(1), "message is sent") {
player.sendMessage(startsWith("You dig a hole"))
}
}
@ParameterizedTest
@EnumSource(value = Bone::class)
fun `Burying a bone should play an animation`(bone: Bone) {
player.inventory.add(bone.id)
player.interactWithItem(bone.id, option = 1)
verifyAfter(action.ticks(1), "animation is played") {
player.playAnimation(eq(BURY_BONE_ANIMATION))
}
}
@ParameterizedTest
@EnumSource(value = Bone::class)
fun `Burying a bone should give the player experience`(bone: Bone) {
player.inventory.add(bone.id)
player.interactWithItem(bone.id, option = 1)
action.ticks(1)
after(action.complete(), "experience is granted after bone burial") {
verify { player.sendMessage(startsWith("You bury the bones")) }
assertEquals(bone.xp, player.prayer.experience)
assertEquals(player.inventory.getAmount(bone.id), 0)
}
}
}
@@ -90,10 +90,7 @@ class WoodcuttingAction(
wait(tool.pulses)
// Check that the object exists in the world
val obj = target.getObject(mob.world)
if (obj == null) {
stop()
}
val obj = target.getObject(mob.world) ?: stop()
if (mob.inventory.add(target.tree.id)) {
val logName = Definitions.item(target.tree.id)!!.name.toLowerCase()
@@ -105,7 +102,7 @@ class WoodcuttingAction(
// respawn time: http://runescape.wikia.com/wiki/Trees
val respawn = TimeUnit.SECONDS.toMillis(MINIMUM_RESPAWN_TIME + rand(150)) / GameConstants.PULSE_DELAY
mob.world.expireObject(obj!!, target.tree.stump, respawn.toInt())
mob.world.expireObject(obj, target.tree.stump, respawn.toInt())
stop()
}
}
@@ -0,0 +1,72 @@
import org.apollo.cache.def.ItemDefinition
import org.apollo.game.model.entity.Player
import org.apollo.game.model.entity.Skill
import org.apollo.game.plugin.skills.woodcutting.Axe
import org.apollo.game.plugin.testing.junit.ApolloTestingExtension
import org.apollo.game.plugin.testing.junit.api.annotations.ItemDefinitions
import org.apollo.game.plugin.testing.junit.api.annotations.TestMock
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.extension.ExtendWith
import org.junit.jupiter.params.ParameterizedTest
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
@ParameterizedTest
@EnumSource(Axe::class)
fun `No axe is chosen if none are available`(axe: Axe) {
player.skillSet.setCurrentLevel(Skill.WOODCUTTING, axe.level)
assertEquals(null, Axe.bestFor(player))
}
@ParameterizedTest
@EnumSource(Axe::class)
fun `The highest level axe is chosen when available`(axe: Axe) {
player.skillSet.setCurrentLevel(Skill.WOODCUTTING, axe.level)
player.inventory.add(axe.id)
assertEquals(axe, Axe.bestFor(player))
}
@ParameterizedTest
@EnumSource(Axe::class)
fun `Only axes the player has are chosen`(axe: Axe) {
player.skillSet.setCurrentLevel(Skill.WOODCUTTING, axe.level)
player.inventory.add(Axe.BRONZE.id)
assertEquals(Axe.BRONZE, Axe.bestFor(player))
}
@ParameterizedTest
@EnumSource(Axe::class)
fun `Axes can be chosen from equipment as well as inventory`(axe: Axe) {
player.skillSet.setCurrentLevel(Skill.WOODCUTTING, axe.level)
player.inventory.add(axe.id)
assertEquals(axe, Axe.bestFor(player))
}
@ParameterizedTest
@EnumSource(Axe::class)
fun `Axes with a level requirement higher than the player's are ignored`(axe: Axe) {
player.skillSet.setCurrentLevel(Skill.WOODCUTTING, axe.level)
player.inventory.add(axe.id)
Axe.values()
.filter { it.level > axe.level }
.forEach { player.inventory.add(it.id) }
assertEquals(axe, Axe.bestFor(player))
}
}
@@ -0,0 +1,17 @@
import org.apollo.game.plugin.skills.woodcutting.Tree
import org.junit.jupiter.api.extension.ExtensionContext
import org.junit.jupiter.params.provider.Arguments
import org.junit.jupiter.params.provider.ArgumentsProvider
import java.util.stream.Stream
data class WoodcuttingTestData(val treeId: Int, val stumpId: Int, val tree: Tree)
fun woodcuttingTestData(): Collection<WoodcuttingTestData> = Tree.values()
.flatMap { tree -> tree.objects.map { WoodcuttingTestData(it, tree.stump, tree) } }
.toList()
class WoodcuttingTestDataProvider : ArgumentsProvider {
override fun provideArguments(context: ExtensionContext?): Stream<out Arguments> {
return woodcuttingTestData().map { Arguments { arrayOf(it) } }.stream()
}
}
@@ -0,0 +1,88 @@
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import org.apollo.cache.def.ItemDefinition
import org.apollo.game.model.entity.Player
import org.apollo.game.model.entity.Skill
import org.apollo.game.plugin.skills.woodcutting.Axe
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.interactWithObject
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assumptions.assumeTrue
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.*
@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
@TestMock
lateinit var player: Player
@ParameterizedTest
@ArgumentsSource(WoodcuttingTestDataProvider::class)
fun `Attempting to cut a tree when the player has no axe should send a message`(data: WoodcuttingTestData) {
player.interactWithObject(data.treeId, 1)
verify { player.sendMessage(contains("do not have an axe")) }
}
@ParameterizedTest
@ArgumentsSource(WoodcuttingTestDataProvider::class)
fun `Attempting to cut a tree when the player is too low levelled should send a message`(data: WoodcuttingTestData) {
assumeTrue(data.tree.level > 1, "Normal trees are covered by axe requirements")
player.inventory.add(Axe.BRONZE.id)
player.skillSet.setCurrentLevel(Skill.WOODCUTTING, data.tree.level - 1)
player.interactWithObject(data.treeId, 1)
verifyAfter(action.complete()) { player.sendMessage(contains("do not have the required level")) }
}
@Disabled("Mocking constructors is not supported in mockk. Update WoodcuttingAction to pass a chance value")
@ParameterizedTest
@ArgumentsSource(WoodcuttingTestDataProvider::class)
fun `Cutting a tree we have the skill to cut should eventually reward logs and experience`(
data: WoodcuttingTestData
) {
// Mock RNG instances used by mining internally to determine success
// @todo - improve this so we don't have to mock Random
val rng = mockk<Random>()
every { rng.nextInt(100) } answers { 0 }
player.inventory.add(Axe.BRONZE.id)
player.skillSet.setCurrentLevel(Skill.WOODCUTTING, data.tree.level)
player.interactWithObject(data.treeId, 1)
verifyAfter(action.ticks(1)) { player.sendMessage(contains("You swing your axe")) }
after(action.ticks(Axe.BRONZE.pulses)) {
// @todo - cummulative ticks() calls?
verify { player.sendMessage("You manage to cut some <tree_type>") }
assertEquals(data.tree.exp, player.skillSet.getExperience(Skill.WOODCUTTING))
assertEquals(1, player.inventory.getAmount(data.tree.id))
}
}
}
+3
View File
@@ -0,0 +1,3 @@
plugin {
name = "entity_lookup"
}
@@ -299,7 +299,7 @@ public final class World {
playerRepository.add(player);
players.put(NameUtil.encodeBase37(username), player);
logger.info("Registered player: " + player + " [count=" + playerRepository.size() + "]");
logger.finest("Registered player: " + player + " [count=" + playerRepository.size() + "]");
}
/**
@@ -359,7 +359,7 @@ public final class World {
region.removeEntity(player);
playerRepository.remove(player);
logger.info("Unregistered player: " + player + " [count=" + playerRepository.size() + "]");
logger.finest("Unregistered player: " + player + " [count=" + playerRepository.size() + "]");
}
/**
@@ -235,6 +235,15 @@ public abstract class Mob extends Entity {
return firstDirection;
}
/**
* Gets the current action, if any, of this mob.
*
* @return The action.
*/
public final Action<?> getAction() {
return action;
}
/**
* Gets the index of this mob.
*
@@ -337,6 +346,15 @@ public abstract class Mob extends Entity {
return definition.map(NpcDefinition::getSize).orElse(1);
}
/**
* Check whether this mob has a current active {@link Action}.
*
* @return {@code true} if this mob has a non-null {@link Action}.
*/
public final boolean hasAction() {
return action != null;
}
/**
* Returns whether or not this mob has an {@link NpcDefinition}.
*
@@ -55,7 +55,7 @@ class ActionCoroutine : Continuation<Unit> {
* Create a new `ActionCoroutine` and immediately execute the given `block`, returning a continuation that
* can be resumed.
*/
fun start(block: ActionBlock) : ActionCoroutine {
fun start(block: ActionBlock): ActionCoroutine {
val coroutine = ActionCoroutine()
val continuation = block.createCoroutineUnchecked(coroutine, coroutine)
@@ -95,10 +95,7 @@ class ActionCoroutine : Continuation<Unit> {
* Update this continuation and check if the condition for the next step to be resumed is satisfied.
*/
fun pulse() {
val nextStep = next.getAndSet(null)
if (nextStep == null) {
return
}
val nextStep = next.getAndSet(null) ?: return
val condition = nextStep.condition
val continuation = nextStep.continuation
@@ -120,11 +117,13 @@ class ActionCoroutine : Continuation<Unit> {
/**
* Stop execution of this continuation.
*/
suspend fun stop() {
return suspendCancellableCoroutine(true) { cont ->
suspend fun stop(): Nothing {
suspendCancellableCoroutine<Unit>(true) { cont ->
next.set(null)
cont.cancel()
}
error("Tried to resume execution a coroutine that should have been cancelled.")
}
/**
@@ -1,7 +1,7 @@
package org.apollo.game.action
import org.apollo.game.action.ActionCoroutine
import org.junit.Assert.*
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class ActionCoroutineTest {
@@ -29,7 +29,6 @@ class ActionCoroutineTest {
fun `Coroutine cancels on stop() calls`() {
val coroutine = ActionCoroutine.start {
stop()
wait(1)
}
assertTrue(coroutine.stopped())
+5
View File
@@ -11,4 +11,9 @@ ext {
commonsCompressVersion = '1.10'
assertjVersion = '3.8.0'
classGraphVersion = '4.0.6'
junitVintageVersion = '5.1.0'
junitPlatformVersion = '1.1.0'
junitJupiterVersion = '5.1.0'
mockkVersion = '1.7.15'
assertkVersion = '0.9'
}