Fix no argument and multiple argument handling

- Added a bunch more command tests
- Fixed no arg command handling
- Fixed multiarg command handling (with data propagation)
- Fixed exception reporting in the command system
This commit is contained in:
Norbi Peti 2023-07-24 15:39:28 +02:00
parent 19362cfe5f
commit 82d43e5b09
No known key found for this signature in database
GPG key ID: DBA4C4549A927E56
7 changed files with 134 additions and 31 deletions

View file

@ -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. * Returns true while unit testing.
*/ */

View file

@ -2,7 +2,6 @@ package buttondevteam.lib.chat
import buttondevteam.core.MainPlugin import buttondevteam.core.MainPlugin
import buttondevteam.lib.ChromaUtils import buttondevteam.lib.ChromaUtils
import buttondevteam.lib.TBMCCoreAPI
import buttondevteam.lib.chat.commands.* import buttondevteam.lib.chat.commands.*
import buttondevteam.lib.chat.commands.CommandUtils.coreArgument import buttondevteam.lib.chat.commands.CommandUtils.coreArgument
import buttondevteam.lib.chat.commands.CommandUtils.coreCommand import buttondevteam.lib.chat.commands.CommandUtils.coreCommand
@ -113,11 +112,7 @@ abstract class Command2<TC : ICommand2<TP>, TP : Command2Sender>(
} catch (e: CommandSyntaxException) { } catch (e: CommandSyntaxException) {
sender.sendMessage(e.message) sender.sendMessage(e.message)
} catch (e: Exception) { } catch (e: Exception) {
TBMCCoreAPI.SendException( ChromaUtils.throwWhenTested(e, "Command execution failed for sender ${sender.name}(${sender.javaClass.canonicalName}) and message $commandline")
"Command execution failed for sender ${sender.name}(${sender.javaClass.canonicalName}) and message $commandline",
e,
MainPlugin.instance
)
} }
} }
if (ChromaUtils.isTest) { if (ChromaUtils.isTest) {
@ -201,21 +196,26 @@ abstract class Command2<TC : ICommand2<TP>, TP : Command2Sender>(
method method
).executes(this::executeHelpText) ).executes(this::executeHelpText)
fun getArgNodes(parent: ArgumentBuilder<TP, *>, params: MutableList<CommandArgument>) { fun getArgNodes(parent: ArgumentBuilder<TP, *>, params: MutableList<CommandArgument>, executable: Boolean) {
// TODO: Implement optional arguments here by making the last non-optional parameter also executable // 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 argType = getArgumentType(param)
val arg = CoreArgumentBuilder.argument<TP, _>(param.name, argType, param.optional) val arg = CoreArgumentBuilder.argument<TP, _>(param.name, argType, param.optional)
if (params.isEmpty()) { if (params.isEmpty() || executable) {
arg.executes { context: CommandContext<TP> -> executeCommand(context) } arg.executes { context: CommandContext<TP> -> executeCommand(context) }
} else { } else {
arg.executes(::executeHelpText) arg.executes(::executeHelpText)
getArgNodes(arg, params) }
if (params.isNotEmpty()) {
getArgNodes(arg, params, param.optional)
} }
parent.then(arg) parent.then(arg)
} }
if (params.isNotEmpty()) { 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") return node.build().coreExecutable() ?: throw IllegalStateException("Command node should be executable but isn't: $fullPath")
} }
@ -329,12 +329,13 @@ abstract class Command2<TC : ICommand2<TP>, TP : Command2Sender>(
* @return Vanilla command success level (0) * @return Vanilla command success level (0)
*/ */
protected open fun executeCommand(context: CommandContext<TP>): Int { protected open fun executeCommand(context: CommandContext<TP>): Int {
assert(context.nodes.lastOrNull()?.node?.coreArgument() != null) // TODO: What if there are no arguments? @Suppress("UNCHECKED_CAST")
val node = context.nodes.last().node.coreArgument()!! val sd = context.nodes.lastOrNull()?.node?.let {
it.coreArgument()?.commandData as SubcommandData<TC, TP>?
?: it.coreCommand<_, SubcommandData<TC, TP>>()?.data
} ?: throw IllegalStateException("Could not find suitable command node for command ${context.input}")
val sender = context.source val sender = context.source
@Suppress("UNCHECKED_CAST")
val sd = node.commandData as SubcommandData<TC, TP>
if (!sd.hasPermission(sender)) { if (!sd.hasPermission(sender)) {
sender.sendMessage("${ChatColor.RED}You don't have permission to use this command") sender.sendMessage("${ChatColor.RED}You don't have permission to use this command")
return 1 return 1
@ -398,9 +399,9 @@ abstract class Command2<TC : ICommand2<TP>, TP : Command2Sender>(
} else if (ret != null) } else if (ret != null)
throw Exception("Wrong return type! Must return a boolean or void. Return value: $ret") throw Exception("Wrong return type! Must return a boolean or void. Return value: $ret")
} catch (e: InvocationTargetException) { } 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) { } 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) if (runOnPrimaryThread && !ChromaUtils.isTest)

View file

@ -20,7 +20,7 @@ class CoreArgumentBuilder<S : Command2Sender, T>(
} }
override fun build(): CoreArgumentCommandNode<S, T> { override fun build(): CoreArgumentCommandNode<S, T> {
return CoreArgumentCommandNode( val result = CoreArgumentCommandNode(
name, name,
type, type,
command, command,
@ -31,6 +31,10 @@ class CoreArgumentBuilder<S : Command2Sender, T>(
suggestionsProvider, suggestionsProvider,
optional optional
) )
for (node in arguments) {
result.addChild(node)
}
return result
} }
override fun then(argument: ArgumentBuilder<S, *>?): CoreArgumentBuilder<S, T> { override fun then(argument: ArgumentBuilder<S, *>?): CoreArgumentBuilder<S, T> {

View file

@ -1,5 +1,6 @@
package buttondevteam.lib.chat package buttondevteam.lib.chat
import buttondevteam.lib.chat.commands.CommandUtils.coreArgument
import buttondevteam.lib.chat.commands.SubcommandData import buttondevteam.lib.chat.commands.SubcommandData
import com.mojang.brigadier.Command import com.mojang.brigadier.Command
import com.mojang.brigadier.RedirectModifier import com.mojang.brigadier.RedirectModifier
@ -15,8 +16,16 @@ class CoreArgumentCommandNode<S : Command2Sender, T>(
val optional: Boolean val optional: Boolean
) : ) :
ArgumentCommandNode<S, T>(name, type, command, requirement, redirect, modifier, forks, customSuggestions) { ArgumentCommandNode<S, T>(name, type, command, requirement, redirect, modifier, forks, customSuggestions) {
lateinit var commandData: SubcommandData<*, S> private var _commandData: SubcommandData<*, S>? = null
internal set // TODO: This should propagate to other arguments 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 { override fun getUsageText(): String {
return if (optional) "[$name]" else "<$name>" return if (optional) "[$name]" else "<$name>"

View file

@ -3,8 +3,6 @@ package buttondevteam.lib.chat
import buttondevteam.lib.chat.Command2.Subcommand import buttondevteam.lib.chat.Command2.Subcommand
import java.lang.reflect.Method import java.lang.reflect.Method
import java.lang.reflect.Modifier 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. * This class is used as a base class for all the specific command implementations.
@ -20,7 +18,6 @@ abstract class ICommand2<TP : Command2Sender>(val manager: Command2<*, TP>) {
* @param sender The sender which ran the command * @param sender The sender which ran the command
* @return The success of the command * @return The success of the command
*/ */
@Suppress("UNUSED_PARAMETER")
open fun def(sender: TP): Boolean { open fun def(sender: TP): Boolean {
return false return false
} }
@ -76,10 +73,6 @@ abstract class ICommand2<TP : Command2Sender>(val manager: Command2<*, TP>) {
private fun getcmdpath(): String { private fun getcmdpath(): String {
if (!javaClass.isAnnotationPresent(CommandClass::class.java)) if (!javaClass.isAnnotationPresent(CommandClass::class.java))
throw RuntimeException("No @CommandClass annotation on command class ${javaClass.simpleName}!") 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<Class<*>>(javaClass) val classList = mutableListOf<Class<*>>(javaClass)
while (true) { while (true) {
val superClass = classList.last().superclass val superClass = classList.last().superclass

View file

@ -17,6 +17,7 @@ import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestMethodOrder import org.junit.jupiter.api.TestMethodOrder
import java.util.* import java.util.*
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertFails
@TestMethodOrder(MethodOrderer.OrderAnnotation::class) @TestMethodOrder(MethodOrderer.OrderAnnotation::class)
class Command2MCTest { class Command2MCTest {
@ -45,6 +46,9 @@ class Command2MCTest {
assertEquals("test", coreExecutable?.data?.argumentsInOrder?.firstOrNull()?.name, "Failed to get correct argument name") 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(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") 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 @Test
@ -66,6 +70,7 @@ class Command2MCTest {
@Order(3) @Order(3)
fun testHandleCommand() { fun testHandleCommand() {
val user = ChromaGamerBase.getUser(UUID.randomUUID().toString(), TBMCPlayer::class.java) val user = ChromaGamerBase.getUser(UUID.randomUUID().toString(), TBMCPlayer::class.java)
user.playerName = "TestPlayer"
val sender = object : Command2MCSender(user, Channel.globalChat, user) { val sender = object : Command2MCSender(user, Channel.globalChat, user) {
override fun sendMessage(message: String) { override fun sendMessage(message: String) {
error(message) error(message)
@ -75,20 +80,77 @@ class Command2MCTest {
error(message.joinToString("\n")) error(message.joinToString("\n"))
} }
} }
assert(ButtonPlugin.command2MC.handleCommand(sender, "/test hmm")) runCommand(sender, "/test hmm", TestCommand, "hmm")
assertEquals("hmm", testCommandReceived) 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 @CommandClass
object TestCommand : ICommand2MC() { object TestCommand : ICommand2MC(), ITestCommand2MC {
override var testCommandReceived: String? = null
@Command2.Subcommand @Command2.Subcommand
fun def(sender: Command2MCSender, test: String) { fun def(sender: Command2MCSender, test: String) {
testCommandReceived = test 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 { companion object {
private var initialized = false private var initialized = false
private var testCommandReceived: String? = null }
interface ITestCommand2MC {
var testCommandReceived: String?
} }
} }

View file

@ -7,6 +7,24 @@ buttondevteam:
def: def:
method: def() method: def()
params: 'test' 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: core:
ComponentCommand: ComponentCommand:
def: def: