diff --git a/Chroma-Core/src/main/java/buttondevteam/lib/ChromaUtils.kt b/Chroma-Core/src/main/java/buttondevteam/lib/ChromaUtils.kt index 415ba65..3a5ae4e 100644 --- a/Chroma-Core/src/main/java/buttondevteam/lib/ChromaUtils.kt +++ b/Chroma-Core/src/main/java/buttondevteam/lib/ChromaUtils.kt @@ -81,6 +81,22 @@ object ChromaUtils { } } + /** + * Throws the exception directly during testing but only reports it when running on the server. + * This way exceptions get reported properly when running, but they can be checked during testing. + * + * Useful in code blocks scheduled using the Bukkit API. + */ + fun throwWhenTested(exception: Throwable, message: String) { + if (isTest) { + // Propagate exception back to the tests + throw exception + } else { + // Otherwise we don't run the code directly, so we need to handle this here + TBMCCoreAPI.SendException(message, exception, MainPlugin.instance) + } + } + /** * Returns true while unit testing. */ diff --git a/Chroma-Core/src/main/java/buttondevteam/lib/chat/Command2.kt b/Chroma-Core/src/main/java/buttondevteam/lib/chat/Command2.kt index 1d18a8d..16f8f67 100644 --- a/Chroma-Core/src/main/java/buttondevteam/lib/chat/Command2.kt +++ b/Chroma-Core/src/main/java/buttondevteam/lib/chat/Command2.kt @@ -2,7 +2,6 @@ package buttondevteam.lib.chat import buttondevteam.core.MainPlugin import buttondevteam.lib.ChromaUtils -import buttondevteam.lib.TBMCCoreAPI import buttondevteam.lib.chat.commands.* import buttondevteam.lib.chat.commands.CommandUtils.coreArgument import buttondevteam.lib.chat.commands.CommandUtils.coreCommand @@ -113,11 +112,7 @@ abstract class Command2, TP : Command2Sender>( } catch (e: CommandSyntaxException) { sender.sendMessage(e.message) } catch (e: Exception) { - TBMCCoreAPI.SendException( - "Command execution failed for sender ${sender.name}(${sender.javaClass.canonicalName}) and message $commandline", - e, - MainPlugin.instance - ) + ChromaUtils.throwWhenTested(e, "Command execution failed for sender ${sender.name}(${sender.javaClass.canonicalName}) and message $commandline") } } if (ChromaUtils.isTest) { @@ -201,21 +196,26 @@ abstract class Command2, TP : Command2Sender>( method ).executes(this::executeHelpText) - fun getArgNodes(parent: ArgumentBuilder, params: MutableList) { + fun getArgNodes(parent: ArgumentBuilder, params: MutableList, executable: Boolean) { // TODO: Implement optional arguments here by making the last non-optional parameter also executable - val param = params.removeLast() + val param = params.removeFirst() val argType = getArgumentType(param) val arg = CoreArgumentBuilder.argument(param.name, argType, param.optional) - if (params.isEmpty()) { + if (params.isEmpty() || executable) { arg.executes { context: CommandContext -> executeCommand(context) } } else { arg.executes(::executeHelpText) - getArgNodes(arg, params) + } + if (params.isNotEmpty()) { + getArgNodes(arg, params, param.optional) } parent.then(arg) } + if (params.isNotEmpty()) { - getArgNodes(node, params.toMutableList()) + getArgNodes(node, params.toMutableList(), false) + } else { + node.executes(::executeCommand) } return node.build().coreExecutable() ?: throw IllegalStateException("Command node should be executable but isn't: $fullPath") } @@ -329,12 +329,13 @@ abstract class Command2, TP : Command2Sender>( * @return Vanilla command success level (0) */ protected open fun executeCommand(context: CommandContext): Int { - assert(context.nodes.lastOrNull()?.node?.coreArgument() != null) // TODO: What if there are no arguments? - val node = context.nodes.last().node.coreArgument()!! + @Suppress("UNCHECKED_CAST") + val sd = context.nodes.lastOrNull()?.node?.let { + it.coreArgument()?.commandData as SubcommandData? + ?: it.coreCommand<_, SubcommandData>()?.data + } ?: throw IllegalStateException("Could not find suitable command node for command ${context.input}") val sender = context.source - @Suppress("UNCHECKED_CAST") - val sd = node.commandData as SubcommandData if (!sd.hasPermission(sender)) { sender.sendMessage("${ChatColor.RED}You don't have permission to use this command") return 1 @@ -398,9 +399,9 @@ abstract class Command2, TP : Command2Sender>( } else if (ret != null) throw Exception("Wrong return type! Must return a boolean or void. Return value: $ret") } catch (e: InvocationTargetException) { - TBMCCoreAPI.SendException("An error occurred in a command handler for ${sd.fullPath}!", e.cause ?: e, MainPlugin.instance) + ChromaUtils.throwWhenTested(e.cause ?: e, "An error occurred in a command handler for ${sd.fullPath}!") } catch (e: Exception) { - TBMCCoreAPI.SendException("Command handling failed for sender $sender and subcommand ${sd.fullPath}", e, MainPlugin.instance) + ChromaUtils.throwWhenTested(e, "Command handling failed for sender $sender and subcommand ${sd.fullPath}") } } if (runOnPrimaryThread && !ChromaUtils.isTest) diff --git a/Chroma-Core/src/main/java/buttondevteam/lib/chat/CoreArgumentBuilder.kt b/Chroma-Core/src/main/java/buttondevteam/lib/chat/CoreArgumentBuilder.kt index eadc65a..9228fee 100644 --- a/Chroma-Core/src/main/java/buttondevteam/lib/chat/CoreArgumentBuilder.kt +++ b/Chroma-Core/src/main/java/buttondevteam/lib/chat/CoreArgumentBuilder.kt @@ -20,7 +20,7 @@ class CoreArgumentBuilder( } override fun build(): CoreArgumentCommandNode { - return CoreArgumentCommandNode( + val result = CoreArgumentCommandNode( name, type, command, @@ -31,6 +31,10 @@ class CoreArgumentBuilder( suggestionsProvider, optional ) + for (node in arguments) { + result.addChild(node) + } + return result } override fun then(argument: ArgumentBuilder?): CoreArgumentBuilder { diff --git a/Chroma-Core/src/main/java/buttondevteam/lib/chat/CoreArgumentCommandNode.kt b/Chroma-Core/src/main/java/buttondevteam/lib/chat/CoreArgumentCommandNode.kt index 5e81498..d778e01 100644 --- a/Chroma-Core/src/main/java/buttondevteam/lib/chat/CoreArgumentCommandNode.kt +++ b/Chroma-Core/src/main/java/buttondevteam/lib/chat/CoreArgumentCommandNode.kt @@ -1,5 +1,6 @@ package buttondevteam.lib.chat +import buttondevteam.lib.chat.commands.CommandUtils.coreArgument import buttondevteam.lib.chat.commands.SubcommandData import com.mojang.brigadier.Command import com.mojang.brigadier.RedirectModifier @@ -15,8 +16,16 @@ class CoreArgumentCommandNode( val optional: Boolean ) : ArgumentCommandNode(name, type, command, requirement, redirect, modifier, forks, customSuggestions) { - lateinit var commandData: SubcommandData<*, S> - internal set // TODO: This should propagate to other arguments + private var _commandData: SubcommandData<*, S>? = null + var commandData: SubcommandData<*, S> + get() { + return _commandData + ?: throw UninitializedPropertyAccessException("Command data has not been initialized") + } + internal set(value) { + _commandData = value + children.forEach { it.coreArgument()?.commandData = value } + } override fun getUsageText(): String { return if (optional) "[$name]" else "<$name>" diff --git a/Chroma-Core/src/main/java/buttondevteam/lib/chat/ICommand2.kt b/Chroma-Core/src/main/java/buttondevteam/lib/chat/ICommand2.kt index b8d724a..723ce90 100644 --- a/Chroma-Core/src/main/java/buttondevteam/lib/chat/ICommand2.kt +++ b/Chroma-Core/src/main/java/buttondevteam/lib/chat/ICommand2.kt @@ -3,8 +3,6 @@ package buttondevteam.lib.chat import buttondevteam.lib.chat.Command2.Subcommand import java.lang.reflect.Method import java.lang.reflect.Modifier -import java.util.* -import java.util.function.Function /** * This class is used as a base class for all the specific command implementations. @@ -20,7 +18,6 @@ abstract class ICommand2(val manager: Command2<*, TP>) { * @param sender The sender which ran the command * @return The success of the command */ - @Suppress("UNUSED_PARAMETER") open fun def(sender: TP): Boolean { return false } @@ -76,10 +73,6 @@ abstract class ICommand2(val manager: Command2<*, TP>) { private fun getcmdpath(): String { if (!javaClass.isAnnotationPresent(CommandClass::class.java)) throw RuntimeException("No @CommandClass annotation on command class ${javaClass.simpleName}!") - val getFromClass = Function { cl: Class<*> -> - cl.simpleName.lowercase(Locale.getDefault()).replace("commandbase", "") // <-- ... - .replace("command", "") - } val classList = mutableListOf>(javaClass) while (true) { val superClass = classList.last().superclass diff --git a/Chroma-Core/src/test/kotlin/buttondevteam/lib/chat/test/Command2MCTest.kt b/Chroma-Core/src/test/kotlin/buttondevteam/lib/chat/test/Command2MCTest.kt index a40e2ce..2cb5020 100644 --- a/Chroma-Core/src/test/kotlin/buttondevteam/lib/chat/test/Command2MCTest.kt +++ b/Chroma-Core/src/test/kotlin/buttondevteam/lib/chat/test/Command2MCTest.kt @@ -17,6 +17,7 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestMethodOrder import java.util.* import kotlin.test.assertEquals +import kotlin.test.assertFails @TestMethodOrder(MethodOrderer.OrderAnnotation::class) class Command2MCTest { @@ -45,6 +46,9 @@ class Command2MCTest { assertEquals("test", coreExecutable?.data?.argumentsInOrder?.firstOrNull()?.name, "Failed to get correct argument name") assertEquals(String::class.java, coreExecutable?.data?.arguments?.get("test")?.type, "The argument could not be found or type doesn't match") assertEquals(Command2MCSender::class.java, coreExecutable?.data?.senderType, "The sender's type doesn't seem to be stored correctly") + MainPlugin.instance.registerCommand(NoArgTestCommand) + assertEquals("No sender parameter for method '${ErroringTestCommand::class.java.getMethod("def")}'", assertFails { MainPlugin.instance.registerCommand(ErroringTestCommand) }.message) + MainPlugin.instance.registerCommand(MultiArgTestCommand) } @Test @@ -66,6 +70,7 @@ class Command2MCTest { @Order(3) fun testHandleCommand() { val user = ChromaGamerBase.getUser(UUID.randomUUID().toString(), TBMCPlayer::class.java) + user.playerName = "TestPlayer" val sender = object : Command2MCSender(user, Channel.globalChat, user) { override fun sendMessage(message: String) { error(message) @@ -75,20 +80,77 @@ class Command2MCTest { error(message.joinToString("\n")) } } - assert(ButtonPlugin.command2MC.handleCommand(sender, "/test hmm")) - assertEquals("hmm", testCommandReceived) + runCommand(sender, "/test hmm", TestCommand, "hmm") + runCommand(sender, "/noargtest", NoArgTestCommand, "TestPlayer") + assertFails { ButtonPlugin.command2MC.handleCommand(sender, "/noargtest failing") } + runFailingCommand(sender, "/erroringtest") + runCommand(sender, "/multiargtest hmm mhm", MultiArgTestCommand, "hmmmhm") + runCommand(sender, "/multiargtest test2 true 19", MultiArgTestCommand, "true 19") + // TODO: Add expected failed param conversions and missing params + } + + private fun runCommand(sender: Command2MCSender, command: String, obj: ITestCommand2MC, expected: String) { + assert(ButtonPlugin.command2MC.handleCommand(sender, command)) { "Could not find command $command" } + assertEquals(expected, obj.testCommandReceived) + } + + private fun runFailingCommand(sender: Command2MCSender, command: String) { + assert(!ButtonPlugin.command2MC.handleCommand(sender, command)) { "Could execute command $command that shouldn't work" } } @CommandClass - object TestCommand : ICommand2MC() { + object TestCommand : ICommand2MC(), ITestCommand2MC { + override var testCommandReceived: String? = null + @Command2.Subcommand fun def(sender: Command2MCSender, test: String) { testCommandReceived = test } } + @CommandClass + object NoArgTestCommand : ICommand2MC(), ITestCommand2MC { + override var testCommandReceived: String? = null + + @Command2.Subcommand + override fun def(sender: Command2MCSender): Boolean { + testCommandReceived = sender.name + return true + } + + @Command2.Subcommand + fun failing(sender: Command2MCSender): Boolean { + return false + } + } + + @CommandClass + object ErroringTestCommand : ICommand2MC() { + @Command2.Subcommand + fun def() { + } + } + + @CommandClass + object MultiArgTestCommand : ICommand2MC(), ITestCommand2MC { + override var testCommandReceived: String? = null + + @Command2.Subcommand + fun def(sender: Command2MCSender, test: String, test2: String) { + testCommandReceived = test + test2 + } + + @Command2.Subcommand + fun test2(sender: Command2MCSender, btest: Boolean, ntest: Int) { + testCommandReceived = "$btest $ntest" + } + } + companion object { private var initialized = false - private var testCommandReceived: String? = null + } + + interface ITestCommand2MC { + var testCommandReceived: String? } } \ No newline at end of file diff --git a/Chroma-Core/src/test/resources/commands.yml b/Chroma-Core/src/test/resources/commands.yml index bbf8960..49509ce 100644 --- a/Chroma-Core/src/test/resources/commands.yml +++ b/Chroma-Core/src/test/resources/commands.yml @@ -7,6 +7,24 @@ buttondevteam: def: method: def() params: 'test' + NoArgTestCommand: + def: + method: def() + params: '' + failing: + method: failing() + params: '' + ErroringTestCommand: + def: + method: def() + params: '' + MultiArgTestCommand: + def: + method: def() + params: "test test2" + test2: + method: test2() + params: "btest ntest" core: ComponentCommand: def: