From 182de0330f4ee1e56f99d9bd8ecab6ba4f358b46 Mon Sep 17 00:00:00 2001 From: Gary Tierney Date: Tue, 20 Jun 2017 02:13:47 +0100 Subject: [PATCH] Allow multiple players per test Refactors the test helpers to use receiver functions so multiple players can exist in the world per test case. The AddFriendsTest is an example of where this is needed. Additionally, Hamcrest has been replaced with AssertJ for fluent assertions. --- game/build.gradle | 4 +- .../game/message/impl/SendFriendMessage.java | 10 +- .../r317/SendFriendMessageEncoder.java | 2 +- .../r377/SendFriendMessageEncoder.java | 2 +- .../game/plugin/testing/KotlinPluginTest.kt | 34 ++++++ .../plugin/testing/KotlinPluginTestHelpers.kt | 111 +++++++++++++++++ .../testing/fakes/FakePluginContextFactory.kt | 21 ++++ .../testing/mockito/KotlinArgMatcher.kt | 25 ++++ .../mockito/KotlinMockitoExtensions.kt | 14 +++ .../game/plugins/testing/KotlinPluginTest.kt | 113 ------------------ game/src/plugins/bank/test/OpenBankTest.kt | 14 +-- .../private-messaging/src/friends.plugin.kts | 28 ++--- .../plugins/dummy/test/TrainingDummyTest.kt | 26 ++-- game/src/plugins/stub/stub.kt | 29 ----- .../plugins/util/lookup/test/LookupTests.kt | 7 +- 15 files changed, 254 insertions(+), 186 deletions(-) create mode 100644 game/src/pluginTesting/kotlin/org/apollo/game/plugin/testing/KotlinPluginTest.kt create mode 100644 game/src/pluginTesting/kotlin/org/apollo/game/plugin/testing/KotlinPluginTestHelpers.kt create mode 100644 game/src/pluginTesting/kotlin/org/apollo/game/plugin/testing/fakes/FakePluginContextFactory.kt create mode 100644 game/src/pluginTesting/kotlin/org/apollo/game/plugin/testing/mockito/KotlinArgMatcher.kt create mode 100644 game/src/pluginTesting/kotlin/org/apollo/game/plugin/testing/mockito/KotlinMockitoExtensions.kt delete mode 100644 game/src/pluginTesting/kotlin/org/apollo/game/plugins/testing/KotlinPluginTest.kt delete mode 100644 game/src/plugins/stub/stub.kt diff --git a/game/build.gradle b/game/build.gradle index 7c5c8f0f..b9646593 100644 --- a/game/build.gradle +++ b/game/build.gradle @@ -33,9 +33,7 @@ dependencies { compile group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib-jre8', version: "$kotlinVersion" compile group: 'org.jetbrains.kotlin', name: 'kotlin-compiler-embeddable', version: "$kotlinVersion" - // https://mvnrepository.com/artifact/org.hamcrest/hamcrest-all - testCompile group: 'org.hamcrest', name: 'hamcrest-all', version: '1.3' - testCompile "org.jetbrains.kotlin:kotlin-test-junit:$kotlinVersion" + testCompile group: 'org.assertj', name: 'assertj-core', version: '3.8.0' } task run(type: JavaExec, dependsOn: classes) { diff --git a/game/src/main/java/org/apollo/game/message/impl/SendFriendMessage.java b/game/src/main/java/org/apollo/game/message/impl/SendFriendMessage.java index 2900ab24..b86353c1 100644 --- a/game/src/main/java/org/apollo/game/message/impl/SendFriendMessage.java +++ b/game/src/main/java/org/apollo/game/message/impl/SendFriendMessage.java @@ -27,7 +27,7 @@ public final class SendFriendMessage extends Message { */ public SendFriendMessage(String username, int world) { this.username = username; - this.world = world == 0 ? 0 : world + 9; + this.world = world; } /** @@ -48,4 +48,12 @@ public final class SendFriendMessage extends Message { return world; } + /** + * Gets the encoded world id to be sent to the client. + * + * @return The encoded world id. + */ + public int getEncodedWorld() { + return world == 0 ? 0 : world + 9; + } } \ No newline at end of file diff --git a/game/src/main/java/org/apollo/game/release/r317/SendFriendMessageEncoder.java b/game/src/main/java/org/apollo/game/release/r317/SendFriendMessageEncoder.java index 15660fcb..932dca8e 100644 --- a/game/src/main/java/org/apollo/game/release/r317/SendFriendMessageEncoder.java +++ b/game/src/main/java/org/apollo/game/release/r317/SendFriendMessageEncoder.java @@ -18,7 +18,7 @@ public final class SendFriendMessageEncoder extends MessageEncoder()) + + player = world.spawnPlayer("testPlayer") + } + +} diff --git a/game/src/pluginTesting/kotlin/org/apollo/game/plugin/testing/KotlinPluginTestHelpers.kt b/game/src/pluginTesting/kotlin/org/apollo/game/plugin/testing/KotlinPluginTestHelpers.kt new file mode 100644 index 00000000..b6a26f66 --- /dev/null +++ b/game/src/pluginTesting/kotlin/org/apollo/game/plugin/testing/KotlinPluginTestHelpers.kt @@ -0,0 +1,111 @@ +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) -> Boolean = { _ -> true }, timeout: Int = 15) { + val actionCaptor: ArgumentCaptor> = ArgumentCaptor.forClass(Action::class.java) + Mockito.verify(this).startAction(actionCaptor.capture()) + + val action: Action = actionCaptor.value as Action + Assert.assertTrue("Found wrong action type", predicate.invoke(action)) + + var pulses = 0 + + do { + action.pulse() + } 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)) + } + } + +} + diff --git a/game/src/pluginTesting/kotlin/org/apollo/game/plugin/testing/fakes/FakePluginContextFactory.kt b/game/src/pluginTesting/kotlin/org/apollo/game/plugin/testing/fakes/FakePluginContextFactory.kt new file mode 100644 index 00000000..aac82329 --- /dev/null +++ b/game/src/pluginTesting/kotlin/org/apollo/game/plugin/testing/fakes/FakePluginContextFactory.kt @@ -0,0 +1,21 @@ +package org.apollo.game.plugin.testing.fakes + +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 { invocation: InvocationOnMock -> + messageHandlers.putHandler( + invocation.arguments[0] as Class, + invocation.arguments[1] as MessageHandler<*>) + } + + return PowerMockito.mock(PluginContext::class.java, answer) + } +} \ No newline at end of file diff --git a/game/src/pluginTesting/kotlin/org/apollo/game/plugin/testing/mockito/KotlinArgMatcher.kt b/game/src/pluginTesting/kotlin/org/apollo/game/plugin/testing/mockito/KotlinArgMatcher.kt new file mode 100644 index 00000000..74814626 --- /dev/null +++ b/game/src/pluginTesting/kotlin/org/apollo/game/plugin/testing/mockito/KotlinArgMatcher.kt @@ -0,0 +1,25 @@ +package org.apollo.game.plugin.testing.mockito + +import org.mockito.ArgumentMatcher +import java.lang.AssertionError +import java.util.function.Consumer + +class KotlinArgMatcher(val consumer: Consumer) : ArgumentMatcher() { + 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 ?: "" + } +} + diff --git a/game/src/pluginTesting/kotlin/org/apollo/game/plugin/testing/mockito/KotlinMockitoExtensions.kt b/game/src/pluginTesting/kotlin/org/apollo/game/plugin/testing/mockito/KotlinMockitoExtensions.kt new file mode 100644 index 00000000..0bfad491 --- /dev/null +++ b/game/src/pluginTesting/kotlin/org/apollo/game/plugin/testing/mockito/KotlinMockitoExtensions.kt @@ -0,0 +1,14 @@ +package org.apollo.game.plugin.testing.mockito + +import org.mockito.Mockito +import java.util.function.Consumer + + +object KotlinMockitoExtensions { + inline fun matches(crossinline callback: T.() -> Unit): T { + val consumer = Consumer { it.callback() } + val matcher = KotlinArgMatcher(consumer) + + return Mockito.argThat(matcher) + } +} diff --git a/game/src/pluginTesting/kotlin/org/apollo/game/plugins/testing/KotlinPluginTest.kt b/game/src/pluginTesting/kotlin/org/apollo/game/plugins/testing/KotlinPluginTest.kt deleted file mode 100644 index 827d8804..00000000 --- a/game/src/pluginTesting/kotlin/org/apollo/game/plugins/testing/KotlinPluginTest.kt +++ /dev/null @@ -1,113 +0,0 @@ -package org.apollo.game.plugins.testing - -import org.apollo.cache.def.NpcDefinition -import org.apollo.game.action.Action -import org.apollo.game.message.handler.MessageHandler -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.game.plugin.* -import org.apollo.net.message.Message -import org.apollo.util.security.PlayerCredentials -import org.junit.Assert -import org.junit.Before -import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor -import org.mockito.Matchers.any -import org.mockito.Mockito -import org.mockito.invocation.InvocationOnMock -import org.mockito.stubbing.Answer -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 { - lateinit var world: World - lateinit var player: Player - lateinit var messageHandlers: MessageHandlerChainSet - - @Before - fun setup() { - messageHandlers = MessageHandlerChainSet() - world = PowerMockito.spy(World()) - - val answer = Answer { invocation: InvocationOnMock -> - messageHandlers.putHandler( - invocation.arguments[0] as Class, - invocation.arguments[1] as MessageHandler<*>) - } - - val pluginContext = PowerMockito.mock(PluginContext::class.java, answer) - val pluginEnvironment = KotlinPluginEnvironment(world) - - pluginEnvironment.setContext(pluginContext) - pluginEnvironment.load(ArrayList()) - - val credentials = PlayerCredentials("test", "test", 1, 1, "0.0.0.0") - val position = Position(3200, 3200, 0) - val region = world.regionRepository.fromPosition(position) - - player = PowerMockito.spy(Player(world, credentials, position)) - world.register(player) - region.addEntity(player) - - PowerMockito.doNothing().`when`(player).send(any()) - } - - fun interactWith(entity: Entity, option: Int = 1) { - player.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)) - } - } - - fun spawnNpc(id: Int, position: Position): Npc { - val definition = NpcDefinition(id) - val npc = Npc(world, position, definition, arrayOfNulls(4)) - val region = world.regionRepository.fromPosition(position) - val npcs = world.npcRepository - - npcs.add(npc) - region.addEntity(npc) - - return npc - } - - fun spawnObject(id: Int, position: Position): GameObject { - val obj = StaticGameObject(world, id, position, 0, 0) - - world.spawn(obj) - - return obj - } - - fun notify(message: Message) { - messageHandlers.notify(player, message) - } - - fun waitForActionCompletion(predicate: (Action) -> Boolean = { _ -> true }, timeout: Int = 15) { - val actionCaptor: ArgumentCaptor> = ArgumentCaptor.forClass(Action::class.java) - Mockito.verify(player).startAction(actionCaptor.capture()) - - val action: Action = actionCaptor.value as Action - Assert.assertTrue("Found wrong action type", predicate.invoke(action)) - - var pulses = 0 - - do { - action.pulse() - } while (action.isRunning && pulses++ < timeout) - - Assert.assertFalse("Exceeded timeout waiting for action completion", pulses > timeout) - } - -} diff --git a/game/src/plugins/bank/test/OpenBankTest.kt b/game/src/plugins/bank/test/OpenBankTest.kt index 8bede6be..d7b4ed1c 100644 --- a/game/src/plugins/bank/test/OpenBankTest.kt +++ b/game/src/plugins/bank/test/OpenBankTest.kt @@ -1,5 +1,5 @@ import org.apollo.game.model.Position -import org.apollo.game.plugins.testing.KotlinPluginTest +import org.apollo.game.plugin.testing.KotlinPluginTest import org.junit.Test import org.mockito.Mockito.verify @@ -14,21 +14,21 @@ class OpenBankTest() : KotlinPluginTest() { @Test fun `Interacting with a bank teller should open the players bank`() { - val bankTeller = spawnNpc(BANK_TELLER_ID, BANK_POSITION) + 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 - interactWith(bankTeller, option = 2) - waitForActionCompletion() + player.interactWith(bankTeller, option = 2) + player.waitForActionCompletion() verify(player).openBank() } @Test fun `Interacting with a bank booth object should open the players bank`() { - val bankBooth = spawnObject(BANK_BOOTH_ID, BANK_POSITION) + val bankBooth = world.spawnObject(BANK_BOOTH_ID, BANK_POSITION) - interactWith(bankBooth, option = 2) - waitForActionCompletion() + player.interactWith(bankBooth, option = 2) + player.waitForActionCompletion() verify(player).openBank() } diff --git a/game/src/plugins/chat/private-messaging/src/friends.plugin.kts b/game/src/plugins/chat/private-messaging/src/friends.plugin.kts index ea7ced88..6ac357d7 100644 --- a/game/src/plugins/chat/private-messaging/src/friends.plugin.kts +++ b/game/src/plugins/chat/private-messaging/src/friends.plugin.kts @@ -3,21 +3,19 @@ import org.apollo.game.message.impl.SendFriendMessage import org.apollo.game.model.entity.setting.PrivacyState on { AddFriendMessage::class } - .then { - it.addFriend(username) + .then { + it.addFriend(username) - val playerUsername = it.username - val friend = it.world.getPlayer(username) + val friend = it.world.getPlayer(username) - if (friend == null) { - it.send(SendFriendMessage(username, 0)) - } else if (friend.friendsWith(playerUsername) || friend.friendPrivacy == PrivacyState.ON) { - if (it.friendPrivacy != PrivacyState.OFF) { - friend.send(SendFriendMessage(playerUsername, it.worldId)) - } - - if (friend.friendPrivacy != PrivacyState.OFF) { - it.send(SendFriendMessage(username, friend.worldId)) - } - } + if (friend == null || friend.friendPrivacy == PrivacyState.OFF) { + it.send(SendFriendMessage(username, 0)) + return@then + } else { + it.send(SendFriendMessage(username, friend.worldId)) } + + if (friend.friendsWith(it.username) && it.friendPrivacy != PrivacyState.OFF) { + friend.send(SendFriendMessage(it.username, it.worldId)) + } + } diff --git a/game/src/plugins/dummy/test/TrainingDummyTest.kt b/game/src/plugins/dummy/test/TrainingDummyTest.kt index 0fc2da24..2e99e313 100644 --- a/game/src/plugins/dummy/test/TrainingDummyTest.kt +++ b/game/src/plugins/dummy/test/TrainingDummyTest.kt @@ -1,10 +1,10 @@ import org.apollo.game.model.Position import org.apollo.game.model.entity.Skill -import org.apollo.game.plugins.testing.KotlinPluginTest -import org.hamcrest.Matchers.* -import org.junit.Assert.* +import org.apollo.game.plugin.testing.* +import org.assertj.core.api.Assertions.assertThat import org.junit.Test -import org.mockito.Mockito.* +import org.mockito.Mockito.contains +import org.mockito.Mockito.verify class TrainingDummyTest : KotlinPluginTest() { @@ -14,30 +14,30 @@ class TrainingDummyTest : KotlinPluginTest() { } @Test fun `Hitting the training dummy should give the player attack experience`() { - val dummy = spawnObject(DUMMY_ID, DUMMY_POSITION) + val dummy = world.spawnObject(DUMMY_ID, DUMMY_POSITION) val skills = player.skillSet val beforeExp = skills.getExperience(Skill.ATTACK) - interactWith(dummy, option = 2) - waitForActionCompletion() + player.interactWith(dummy, option = 2) + player.waitForActionCompletion() val afterExp = skills.getExperience(Skill.ATTACK) - assertThat(afterExp, greaterThan(beforeExp)) + assertThat(afterExp).isGreaterThan(beforeExp) } @Test fun `The player should stop getting attack experience from the training dummy at level 8`() { - val dummy = spawnObject(DUMMY_ID, DUMMY_POSITION) + val dummy = world.spawnObject(DUMMY_ID, DUMMY_POSITION) val skills = player.skillSet skills.setMaximumLevel(Skill.ATTACK, 8) val beforeExp = skills.getExperience(Skill.ATTACK) - interactWith(dummy, option = 2) - waitForActionCompletion() + player.interactWith(dummy, option = 2) + player.waitForActionCompletion() val afterExp = skills.getExperience(Skill.ATTACK) verify(player).sendMessage(contains("nothing more you can learn")) - assertThat(afterExp, equalTo(beforeExp)) + assertThat(afterExp).isEqualTo(beforeExp) } -} \ No newline at end of file +} diff --git a/game/src/plugins/stub/stub.kt b/game/src/plugins/stub/stub.kt deleted file mode 100644 index f82da254..00000000 --- a/game/src/plugins/stub/stub.kt +++ /dev/null @@ -1,29 +0,0 @@ -/** - * NOTE: This file is a stub, intended only for use within an IDE. It should be updated - * each time [org.apollo.game.plugin.kotlin.KotlinPluginScript] has a new method added to it. - * - * Until IntelliJ IDEA starts to support ScriptTemplateDefinitions this is - * required to resolve references within plugin code. - */ - -import org.apollo.game.model.World -import org.apollo.game.plugin.PluginContext -import org.apollo.game.plugin.kotlin.* -import org.apollo.net.message.Message -import kotlin.reflect.KClass - -fun on(type: () -> KClass): KotlinMessageHandler { - null!! -} - -fun on_command(command: String, privileges: PrivilegeLevel): KotlinCommandHandler { - null!! -} - -fun start(callback: (World) -> Unit) { - -} - -fun stop(callback: (World) -> Unit) { - -} diff --git a/game/src/plugins/util/lookup/test/LookupTests.kt b/game/src/plugins/util/lookup/test/LookupTests.kt index 545cbc41..fc4c18f9 100644 --- a/game/src/plugins/util/lookup/test/LookupTests.kt +++ b/game/src/plugins/util/lookup/test/LookupTests.kt @@ -1,14 +1,15 @@ import org.apollo.cache.def.ItemDefinition +import org.apollo.game.plugin.testing.* +import org.assertj.core.api.Assertions.assertThat import org.junit.Test -import kotlin.test.assertEquals -class LookupTests { +class LookupTests : KotlinPluginTest() { @Test fun itemLookup() { val testItem = ItemDefinition(0) testItem.name = "sword" ItemDefinition.init(arrayOf(testItem)) - assertEquals(testItem, lookup_item("sword")) + assertThat(lookup_item("sword")).isEqualTo(testItem); } } \ No newline at end of file