Add command test, fix various command bugs

This commit is contained in:
Him188 2020-06-28 02:08:44 +08:00
parent b9342be382
commit 32e77ca420
14 changed files with 542 additions and 169 deletions

View File

@ -7,6 +7,8 @@
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
@file:Suppress("WRONG_MODIFIER_CONTAINING_DECLARATION")
package net.mamoe.mirai.console
import kotlinx.coroutines.CoroutineScope
@ -153,10 +155,8 @@ internal interface IMiraiConsole : CoroutineScope {
*/
val builtInPluginLoaders: List<PluginLoader<*, *>>
@Suppress("WRONG_MODIFIER_CONTAINING_DECLARATION")
internal val consoleCommandOwner: ConsoleCommandOwner
@Suppress("WRONG_MODIFIER_CONTAINING_DECLARATION")
internal val consoleCommandSender: ConsoleCommandSender
}

View File

@ -18,7 +18,7 @@ import net.mamoe.mirai.message.data.SingleMessage
* 通常情况下, 你的指令应继承 @see CompositeCommand/SimpleCommand
* @see register 注册这个指令
*
* @see SimpleCommand
* @see RawCommand
* @see CompositeCommand
*/
interface Command {

View File

@ -23,7 +23,6 @@ import net.mamoe.mirai.console.command.internal.*
import net.mamoe.mirai.console.plugin.Plugin
import net.mamoe.mirai.message.data.Message
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.utils.MiraiInternalAPI
/**
* 指令的所有者.
@ -31,9 +30,6 @@ import net.mamoe.mirai.utils.MiraiInternalAPI
*/
sealed class CommandOwner
@MiraiInternalAPI
object TestCommandOwner : CommandOwner()
/**
* 插件指令所有者. 插件只能通过 [PluginCommandOwner] 管理指令.
*/
@ -99,22 +95,29 @@ fun CommandOwner.unregisterAllCommands() {
* @see JCommandManager.register Java 方法
*/
@JvmOverloads
fun Command.register(override: Boolean = false): Boolean = InternalCommandManager.modifyLock.withLock {
if (!override) {
if (findDuplicate() != null) return false
}
InternalCommandManager.registeredCommands.add(this@register)
if (this.prefixOptional) {
for (name in this.names) {
InternalCommandManager.optionalPrefixCommandMap[name] = this
fun Command.register(override: Boolean = false): Boolean {
if (this is CompositeCommand) this.subCommands // init
InternalCommandManager.modifyLock.withLock {
if (!override) {
if (findDuplicate() != null) return false
}
} else {
for (name in this.names) {
InternalCommandManager.optionalPrefixCommandMap.remove(name) // ensure resolution consistency
InternalCommandManager.requiredPrefixCommandMap[name] = this
InternalCommandManager.registeredCommands.add(this@register)
if (this.prefixOptional) {
for (name in this.names) {
val lowerCaseName = name.toLowerCase()
InternalCommandManager.optionalPrefixCommandMap[lowerCaseName] = this
InternalCommandManager.requiredPrefixCommandMap[lowerCaseName] = this
}
} else {
for (name in this.names) {
val lowerCaseName = name.toLowerCase()
InternalCommandManager.optionalPrefixCommandMap.remove(lowerCaseName) // ensure resolution consistency
InternalCommandManager.requiredPrefixCommandMap[lowerCaseName] = this
}
}
return true
}
return true
}
/**
@ -123,7 +126,7 @@ fun Command.register(override: Boolean = false): Boolean = InternalCommandManage
* @see JCommandManager.findDuplicate Java 方法
*/
fun Command.findDuplicate(): Command? =
InternalCommandManager.registeredCommands.firstOrNull { it.names intersects this.names }
InternalCommandManager.registeredCommands.firstOrNull { it.names intersectsIgnoringCase this.names }
/**
* 取消注册这个指令. 若指令未注册, 返回 `false`.
@ -131,9 +134,22 @@ fun Command.findDuplicate(): Command? =
* @see JCommandManager.unregister Java 方法
*/
fun Command.unregister(): Boolean = InternalCommandManager.modifyLock.withLock {
if (this.prefixOptional) {
this.names.forEach {
InternalCommandManager.optionalPrefixCommandMap.remove(it)
}
}
this.names.forEach {
InternalCommandManager.requiredPrefixCommandMap.remove(it)
}
InternalCommandManager.registeredCommands.remove(this)
}
/**
* [this] 已经 [注册][register] 后返回 `true`
*/
fun Command.isRegistered(): Boolean = this in InternalCommandManager.registeredCommands
//// executing without detailed result (faster)
/**
@ -148,7 +164,7 @@ fun Command.unregister(): Boolean = InternalCommandManager.modifyLock.withLock {
*/
suspend fun CommandSender.executeCommand(vararg messages: Any): Command? {
if (messages.isEmpty()) return null
return executeCommandInternal(messages, messages[0].toString().substringBefore(' '))
return matchAndExecuteCommandInternal(messages, messages[0].toString().substringBefore(' '))
}
/**
@ -162,9 +178,46 @@ suspend fun CommandSender.executeCommand(vararg messages: Any): Command? {
@Throws(CommandExecutionException::class)
suspend fun CommandSender.executeCommand(message: MessageChain): Command? {
if (message.isEmpty()) return null
return executeCommandInternal(message, message[0].toString())
return matchAndExecuteCommandInternal(message, message[0].toString().substringBefore(' '))
}
/**
* 执行一个指令
*
* @return 成功执行的指令, 在无匹配指令时返回 `null`
* @throws CommandExecutionException [Command.onCommand] 抛出异常时包装并附带相关指令信息抛出
*
* @see JCommandManager.executeCommand Java 方法
*/
@JvmOverloads
@Throws(CommandExecutionException::class)
suspend fun Command.execute(sender: CommandSender, args: MessageChain, checkPermission: Boolean = true) {
sender.executeCommandInternal(
this,
args.flattenCommandComponents().toTypedArray(),
this.primaryName,
checkPermission
)
}
/**
* 执行一个指令
*
* @return 成功执行的指令, 在无匹配指令时返回 `null`
* @throws CommandExecutionException [Command.onCommand] 抛出异常时包装并附带相关指令信息抛出
*
* @see JCommandManager.executeCommand Java 方法
*/
@JvmOverloads
@Throws(CommandExecutionException::class)
suspend fun Command.execute(sender: CommandSender, vararg args: Any, checkPermission: Boolean = true) {
sender.executeCommandInternal(
this,
args.flattenCommandComponents().toTypedArray(),
this.primaryName,
checkPermission
)
}
//// execution with detailed result

View File

@ -13,6 +13,7 @@ package net.mamoe.mirai.console.command
import kotlinx.coroutines.runBlocking
import net.mamoe.mirai.Bot
import net.mamoe.mirai.console.MiraiConsoleInternal
import net.mamoe.mirai.console.utils.JavaFriendlyAPI
import net.mamoe.mirai.contact.*
import net.mamoe.mirai.message.MessageEvent
@ -64,6 +65,10 @@ suspend inline fun CommandSender.sendMessage(message: String) = sendMessage(Plai
// 前端实现
abstract class ConsoleCommandSender internal constructor() : CommandSender {
final override val bot: Nothing? get() = null
companion object {
internal val instance get() = MiraiConsoleInternal.consoleCommandSender
}
}
fun Friend.asCommandSender(): FriendCommandSender = FriendCommandSender(this)

View File

@ -55,10 +55,10 @@ abstract class CompositeCommand @JvmOverloads constructor(
@Target(AnnotationTarget.FUNCTION)
annotation class Permission(val permission: KClass<out CommandPermission>)
/** 标记一个函数为子指令 */
/** 标记一个函数为子指令, 当 [names] 为空时使用函数名. */
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
annotation class SubCommand(vararg val name: String)
annotation class SubCommand(vararg val names: String)
/** 指令描述 */
@Retention(AnnotationRetention.RUNTIME)
@ -70,6 +70,10 @@ abstract class CompositeCommand @JvmOverloads constructor(
@Target(AnnotationTarget.VALUE_PARAMETER)
annotation class Name(val name: String)
public override suspend fun CommandSender.onDefault(rawArgs: Array<out Any>) {
sendMessage(usage)
}
final override suspend fun CommandSender.onCommand(args: Array<out Any>) {
matchSubCommand(args)?.parseAndExecute(this, args) ?: kotlin.run {
defaultSubCommand.onCommand(this, args)

View File

@ -62,9 +62,22 @@ inline fun CommandArgParser<*>.checkArgument(
@Suppress("FunctionName")
@JvmSynthetic
inline fun <T : Any> CommandArgParser(
crossinline parser: CommandArgParser<T>.(s: String, sender: CommandSender) -> T
crossinline stringParser: CommandArgParser<T>.(s: String, sender: CommandSender) -> T
): CommandArgParser<T> = object : CommandArgParser<T> {
override fun parse(raw: String, sender: CommandSender): T = parser(raw, sender)
override fun parse(raw: String, sender: CommandSender): T = stringParser(raw, sender)
}
/**
* 创建匿名 [CommandArgParser]
*/
@Suppress("FunctionName")
@JvmSynthetic
inline fun <T : Any> CommandArgParser(
crossinline stringParser: CommandArgParser<T>.(s: String, sender: CommandSender) -> T,
crossinline messageParser: CommandArgParser<T>.(m: SingleMessage, sender: CommandSender) -> T
): CommandArgParser<T> = object : CommandArgParser<T> {
override fun parse(raw: String, sender: CommandSender): T = stringParser(raw, sender)
override fun parse(raw: SingleMessage, sender: CommandSender): T = messageParser(raw, sender)
}

View File

@ -127,9 +127,6 @@ class CommandParserContextBuilder : MutableList<ParserPair<*>> by mutableListOf(
inline infix fun <T : Any> KClass<T>.with(parser: CommandArgParser<T>): ParserPair<*> =
ParserPair(this, parser).also { add(it) }
inline infix fun <reified T : Any> auto(parser: CommandArgParser<T>): ParserPair<*> =
ParserPair(T::class, parser).also { add(it) }
/**
* 添加一个指令解析器
*/
@ -147,12 +144,16 @@ class CommandParserContextBuilder : MutableList<ParserPair<*>> by mutableListOf(
crossinline parser: CommandArgParser<T>.(s: String) -> T
): ParserPair<*> = ParserPair(this, CommandArgParser { s: String, _: CommandSender -> parser(s) }).also { add(it) }
@JvmSynthetic
inline fun <reified T : Any> add(parser: CommandArgParser<T>): ParserPair<*> =
ParserPair(T::class, parser).also { add(it) }
/**
* 添加一个指令解析器
*/
@MiraiExperimentalAPI
@JvmSynthetic
inline infix fun <reified T : Any> auto(
inline infix fun <reified T : Any> add(
crossinline parser: CommandArgParser<*>.(s: String) -> T
): ParserPair<*> = T::class with CommandArgParser { s: String, _: CommandSender -> parser(s) }
@ -162,7 +163,7 @@ class CommandParserContextBuilder : MutableList<ParserPair<*>> by mutableListOf(
@MiraiExperimentalAPI
@JvmSynthetic
@LowPriorityInOverloadResolution
inline infix fun <reified T : Any> auto(
inline infix fun <reified T : Any> add(
crossinline parser: CommandArgParser<*>.(s: String, sender: CommandSender) -> T
): ParserPair<*> = T::class with CommandArgParser(parser)
}

View File

@ -27,93 +27,139 @@ internal abstract class CompositeCommandImpl : Command {
override val usage: String // initialized by subCommand reflection
get() = _usage
internal abstract suspend fun CommandSender.onDefault(rawArgs: Array<out Any>)
internal val defaultSubCommand: DefaultSubCommandDescriptor by lazy {
DefaultSubCommandDescriptor(
"",
CommandPermission.Default,
onCommand = block { sender: CommandSender, args: Array<out Any> ->
false//not supported yet
onCommand = block2 { sender: CommandSender, args: Array<out Any> ->
sender.onDefault(args)
}
)
}
internal val subCommands: Array<SubCommandDescriptor> by lazy {
@Suppress("CAST_NEVER_SUCCEEDS")
this as CompositeCommand
this@CompositeCommandImpl as CompositeCommand
val buildUsage = StringBuilder(this.description).append(": \n")
this::class.declaredFunctions.filter { it.hasAnnotation<CompositeCommand.SubCommand>() }.map { function ->
val notStatic = !function.hasAnnotation<JvmStatic>()
val overridePermission = function.findAnnotation<CompositeCommand.Permission>()//optional
val subDescription =
function.findAnnotation<CompositeCommand.Description>()?.description ?: "no description available"
if ((function.returnType.classifier as? KClass<*>)?.isSubclassOf(Boolean::class) != true) {
error("Return Type of SubCommand must be Boolean")
}
val parameters = function.parameters.toMutableList()
check(parameters.isNotEmpty()) {
"First parameter (receiver for kotlin) for sub commend " + function.name + " from " + this.primaryName + " should be <out CommandSender>"
}
if (notStatic) parameters.removeAt(0) // instance
(parameters.removeAt(0)).let { receiver ->
check(!receiver.isVararg && !((receiver.type.classifier as? KClass<*>).also { print(it) }
?.isSubclassOf(CommandSender::class) != true)) {
"First parameter (receiver for kotlin) for sub commend " + function.name + " from " + this.primaryName + " should be <out CommandSender>"
this::class.declaredFunctions.filter { it.hasAnnotation<CompositeCommand.SubCommand>() }
.also { subCommandFunctions ->
// overloading not yet supported
val overloadFunction = subCommandFunctions.groupBy { it.name }.entries.firstOrNull { it.value.size > 1 }
if (overloadFunction != null) {
error("Sub command overloading is not yet supported. (at ${this::class.qualifiedNameOrTip}.${overloadFunction.key})")
}
}
}.map { function ->
val notStatic = !function.hasAnnotation<JvmStatic>()
val overridePermission = function.findAnnotation<CompositeCommand.Permission>()//optional
val subDescription =
function.findAnnotation<CompositeCommand.Description>()?.description ?: "<no description available>"
val commandName = function.findAnnotation<CompositeCommand.SubCommand>()!!.name.map {
if (!it.isValidSubName()) {
error("SubName $it is not valid")
}
it
}.toTypedArray()
//map parameter
val params = parameters.map { param ->
buildUsage.append("/$primaryName ")
if (param.isVararg) error("parameter for sub commend " + function.name + " from " + this.primaryName + " should not be var arg")
if (param.isOptional) error("parameter for sub commend " + function.name + " from " + this.primaryName + " should not be var optional")
val argName = param.findAnnotation<CompositeCommand.Name>()?.name ?: param.name ?: "unknown"
buildUsage.append("<").append(argName).append("> ").append(" ")
CommandParam(
argName,
(param.type.classifier as? KClass<*>)
?: throw IllegalArgumentException("unsolved type reference from param " + param.name + " in " + function.name + " from " + this.primaryName)
)
}.toTypedArray()
buildUsage.append(subDescription).append("\n")
SubCommandDescriptor(
commandName,
params,
subDescription,
overridePermission?.permission?.getInstance() ?: permission,
onCommand = block { sender: CommandSender, args: Array<out Any> ->
if (notStatic) {
function.callSuspend(this, sender, *args) as Boolean
} else {
function.callSuspend(sender, *args) as Boolean
fun KClass<*>.isValidReturnType(): Boolean {
return when (this) {
Boolean::class, Void::class, Unit::class, Nothing::class -> true
else -> false
}
}
)
}.toTypedArray().also {
_usage = buildUsage.toString()
}
check((function.returnType.classifier as? KClass<*>)?.isValidReturnType() == true) {
error("Return type of sub command ${function.name} must be one of the following: kotlin.Boolean, java.lang.Boolean, kotlin.Unit (including implicit), kotlin.Nothing, boolean or void (at ${this::class.qualifiedNameOrTip}.${function.name})")
}
check(!function.returnType.isMarkedNullable) {
error("Return type of sub command ${function.name} must not be marked nullable in Kotlin, and must be marked with @NotNull or @NonNull explicitly in Java. (at ${this::class.qualifiedNameOrTip}.${function.name})")
}
val parameters = function.parameters.toMutableList()
if (notStatic) parameters.removeAt(0) // instance
var hasSenderParam = false
check(parameters.isNotEmpty()) {
"Parameters of sub command ${function.name} must not be empty. (Must have CommandSender as its receiver or first parameter or absent, followed by naturally typed params) (at ${this::class.qualifiedNameOrTip}.${function.name})"
}
parameters.forEach { param ->
check(!param.isVararg) {
"Parameter $param must not be vararg. (at ${this::class.qualifiedNameOrTip}.${function.name}.$param)"
}
}
(parameters.first()).let { receiver ->
if ((receiver.type.classifier as? KClass<*>)?.isSubclassOf(CommandSender::class) == true) {
hasSenderParam = true
parameters.removeAt(0)
}
}
val commandName =
function.findAnnotation<CompositeCommand.SubCommand>()!!.names
.let { namesFromAnnotation ->
if (namesFromAnnotation.isNotEmpty()) {
namesFromAnnotation
} else arrayOf(function.name)
}.also { names ->
names.forEach {
check(it.isValidSubName()) {
"Name of sub command ${function.name} is invalid"
}
}
}
//map parameter
val params = parameters.map { param ->
buildUsage.append("/$primaryName ")
if (param.isOptional) error("optional parameters are not yet supported. (at ${this::class.qualifiedNameOrTip}.${function.name}.$param)")
val argName = param.findAnnotation<CompositeCommand.Name>()?.name ?: param.name ?: "unknown"
buildUsage.append("<").append(argName).append("> ").append(" ")
CommandParam(
argName,
(param.type.classifier as? KClass<*>)
?: throw IllegalArgumentException("unsolved type reference from param " + param.name + ". (at ${this::class.qualifiedNameOrTip}.${function.name}.$param)")
)
}.toTypedArray()
buildUsage.append(subDescription).append("\n")
SubCommandDescriptor(
commandName,
params,
subDescription,
overridePermission?.permission?.getInstance() ?: permission,
onCommand = block { sender: CommandSender, args: Array<out Any> ->
val result = if (notStatic) {
if (hasSenderParam) {
function.isSuspend
function.callSuspend(this, sender, *args)
} else function.callSuspend(this, *args)
} else {
if (hasSenderParam) {
function.callSuspend(sender, *args)
} else function.callSuspend(*args)
}
checkNotNull(result) { "sub command return value is null (at ${this::class.qualifiedName}.${function.name})" }
result as? Boolean ?: true // Unit, void is considered as true.
}
)
}.toTypedArray().also {
_usage = buildUsage.toString()
}
}
private fun block(block: suspend (CommandSender, Array<out Any>) -> Boolean): suspend (CommandSender, Array<out Any>) -> Boolean {
return block
}
private fun block2(block: suspend (CommandSender, Array<out Any>) -> Unit): suspend (CommandSender, Array<out Any>) -> Unit {
return block
}
internal val bakedCommandNameToSubDescriptorArray: Map<Array<String>, SubCommandDescriptor> by lazy {
kotlin.run {
val map = LinkedHashMap<Array<String>, SubCommandDescriptor>(subCommands.size * 2)
@ -129,11 +175,11 @@ internal abstract class CompositeCommandImpl : Command {
internal class DefaultSubCommandDescriptor(
val description: String,
val permission: CommandPermission,
val onCommand: suspend (sender: CommandSender, rawArgs: Array<out Any>) -> Boolean
val onCommand: suspend (sender: CommandSender, rawArgs: Array<out Any>) -> Unit
)
internal inner class SubCommandDescriptor(
val names: Array<String>,
val names: Array<out String>,
val params: Array<CommandParam<*>>,
val description: String,
val permission: CommandPermission,
@ -151,8 +197,7 @@ internal abstract class CompositeCommandImpl : Command {
@JvmField
internal val bakedSubNames: Array<Array<String>> = names.map { it.bakeSubName() }.toTypedArray()
private fun parseArgs(sender: CommandSender, rawArgs: Array<out Any>, offset: Int): Array<out Any> {
@Suppress("CAST_NEVER_SUCCEEDS")
this as CompositeCommand
this@CompositeCommandImpl as CompositeCommand
require(rawArgs.size >= offset + this.params.size) { "No enough args. Required ${params.size}, but given ${rawArgs.size - offset}" }
return Array(this.params.size) { index ->
@ -200,13 +245,13 @@ internal fun String.bakeSubName(): Array<String> = split(' ').filterNot { it.isB
internal fun Any.flattenCommandComponents(): ArrayList<Any> {
val list = ArrayList<Any>()
when (this::class.java) { // faster than is
String::class.java -> (this as String).splitToSequence(' ').filterNot { it.isBlank() }.forEach { list.add(it) }
PlainText::class.java -> (this as PlainText).content.splitToSequence(' ').filterNot { it.isBlank() }
when (this) {
is PlainText -> this.content.splitToSequence(' ').filterNot { it.isBlank() }
.forEach { list.add(it) }
SingleMessage::class.java -> list.add(this as SingleMessage)
Array<Any>::class.java -> (this as Array<*>).forEach { if (it != null) list.addAll(it.flattenCommandComponents()) }
Iterable::class.java -> (this as Iterable<*>).forEach { if (it != null) list.addAll(it.flattenCommandComponents()) }
is CharSequence -> this.splitToSequence(' ').filterNot { it.isBlank() }.forEach { list.add(it) }
is SingleMessage -> list.add(this)
is Array<*> -> this.forEach { if (it != null) list.addAll(it.flattenCommandComponents()) }
is Iterable<*> -> this.forEach { if (it != null) list.addAll(it.flattenCommandComponents()) }
else -> list.add(this.toString())
}
return list
@ -218,3 +263,5 @@ internal inline fun <reified T : Annotation> KAnnotatedElement.hasAnnotation():
internal inline fun <T : Any> KClass<out T>.getInstance(): T {
return this.objectInstance ?: this.createInstance()
}
internal val KClass<*>.qualifiedNameOrTip: String get() = this.qualifiedName ?: "<anonymous class>"

View File

@ -52,24 +52,20 @@ internal object InternalCommandManager {
*/
internal fun matchCommand(rawCommand: String): Command? {
if (rawCommand.startsWith(COMMAND_PREFIX)) {
return requiredPrefixCommandMap[rawCommand.substringAfter(
COMMAND_PREFIX
)]
return requiredPrefixCommandMap[rawCommand.substringAfter(COMMAND_PREFIX).toLowerCase()]
}
return optionalPrefixCommandMap[rawCommand]
return optionalPrefixCommandMap[rawCommand.toLowerCase()]
}
}
internal infix fun <T> Array<out T>.intersects(other: Array<out T>): Boolean {
internal infix fun Array<out String>.intersectsIgnoringCase(other: Array<out String>): Boolean {
val max = this.size.coerceAtMost(other.size)
for (i in 0 until max) {
if (this[i] == other[i]) return true
if (this[i].equals(other[i], ignoreCase = true)) return true
}
return false
}
internal fun String.fuzzyCompare(target: String): Double {
var step = 0
if (this == target) {
@ -169,7 +165,7 @@ internal inline fun <reified T> List<T>.dropToTypedArray(n: Int): Array<T> = Arr
@JvmSynthetic
@Throws(CommandExecutionException::class)
internal suspend inline fun CommandSender.executeCommandInternal(
internal suspend inline fun CommandSender.matchAndExecuteCommandInternal(
messages: Any,
commandName: String
): Command? {
@ -177,7 +173,19 @@ internal suspend inline fun CommandSender.executeCommandInternal(
commandName
) ?: return null
if (!command.testPermission(this)) {
this.executeCommandInternal(command, messages.flattenCommandComponents().dropToTypedArray(1), commandName, true)
return command
}
@JvmSynthetic
@Throws(CommandExecutionException::class)
internal suspend inline fun CommandSender.executeCommandInternal(
command: Command,
args: Array<out Any>,
commandName: String,
checkPermission: Boolean
) {
if (checkPermission && !command.testPermission(this)) {
throw CommandExecutionException(
command,
commandName,
@ -186,13 +194,8 @@ internal suspend inline fun CommandSender.executeCommandInternal(
}
kotlin.runCatching {
command.onCommand(this, messages.flattenCommandComponents().dropToTypedArray(1))
}.fold(
onSuccess = {
return command
},
onFailure = {
throw CommandExecutionException(command, commandName, it)
}
)
}
command.onCommand(this, args)
}.onFailure {
throw CommandExecutionException(command, commandName, it)
}
}

View File

@ -0,0 +1,77 @@
/*
* Copyright 2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.console
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeout
import net.mamoe.mirai.Bot
import net.mamoe.mirai.console.command.ConsoleCommandOwner
import net.mamoe.mirai.console.command.ConsoleCommandSender
import net.mamoe.mirai.console.plugin.PluginLoader
import net.mamoe.mirai.console.plugin.jvm.JarPluginLoader
import net.mamoe.mirai.message.data.Message
import net.mamoe.mirai.utils.DefaultLogger
import net.mamoe.mirai.utils.LoginSolver
import net.mamoe.mirai.utils.MiraiLogger
import java.io.File
import kotlin.coroutines.Continuation
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.resume
import kotlin.test.assertNotNull
fun initTestEnvironment() {
MiraiConsoleInitializer.init(object : IMiraiConsole {
override val rootDir: File = createTempDir()
override val frontEnd: MiraiConsoleFrontEnd = object : MiraiConsoleFrontEnd {
override fun loggerFor(identity: String?): MiraiLogger = DefaultLogger(identity)
override fun pushBot(bot: Bot) = println("pushBot: $bot")
override suspend fun requestInput(hint: String): String = readLine()!!
override fun createLoginSolver(): LoginSolver = LoginSolver.Default
}
override val mainLogger: MiraiLogger = DefaultLogger("main")
override val builtInPluginLoaders: List<PluginLoader<*, *>> = listOf(JarPluginLoader)
override val consoleCommandOwner: ConsoleCommandOwner = object : ConsoleCommandOwner() {}
override val consoleCommandSender: ConsoleCommandSender = object : ConsoleCommandSender() {
override suspend fun sendMessage(message: Message) = println(message)
}
override val coroutineContext: CoroutineContext = SupervisorJob()
})
}
internal object Testing {
@Volatile
internal var cont: Continuation<Any?>? = null
@Suppress("UNCHECKED_CAST")
suspend fun <R> withTesting(timeout: Long = 5000L, block: suspend () -> Unit): R {
@Suppress("RemoveExplicitTypeArguments") // bug
return if (timeout != -1L) {
withTimeout<R>(timeout) {
suspendCancellableCoroutine<R> { ct ->
this@Testing.cont = ct as Continuation<Any?>
runBlocking { block() }
}
}
} else {
suspendCancellableCoroutine<R> { ct ->
this.cont = ct as Continuation<Any?>
runBlocking { block() }
}
}
}
fun ok(result: Any? = Unit) {
val cont = cont
assertNotNull(cont)
cont.resume(result)
}
}

View File

@ -0,0 +1,193 @@
/*
* Copyright 2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
@file:Suppress("unused")
package net.mamoe.mirai.console.command
import kotlinx.coroutines.runBlocking
import net.mamoe.mirai.console.Testing
import net.mamoe.mirai.console.Testing.withTesting
import net.mamoe.mirai.console.command.description.CommandArgParser
import net.mamoe.mirai.console.command.description.CommandParserContext
import net.mamoe.mirai.console.command.internal.InternalCommandManager
import net.mamoe.mirai.console.command.internal.flattenCommandComponents
import net.mamoe.mirai.console.initTestEnvironment
import net.mamoe.mirai.message.data.Image
import net.mamoe.mirai.message.data.SingleMessage
import net.mamoe.mirai.message.data.toMessage
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import kotlin.test.*
object TestCompositeCommand : CompositeCommand(
ConsoleCommandOwner.instance,
"testComposite", "tsC"
) {
@SubCommand
fun mute(seconds: Int) {
Testing.ok(seconds)
}
}
object TestSimpleCommand : RawCommand(owner, "testSimple", "tsS") {
override suspend fun CommandSender.onCommand(args: Array<out Any>) {
Testing.ok(args)
}
}
internal val sender by lazy { ConsoleCommandSender.instance }
internal val owner by lazy { ConsoleCommandOwner.instance }
internal class TestCommand {
companion object {
@JvmStatic
@BeforeAll
fun init() {
initTestEnvironment()
}
}
@Test
fun testRegister() {
try {
assertTrue(TestCompositeCommand.register())
assertFalse(TestCompositeCommand.register())
assertEquals(1, ConsoleCommandOwner.instance.registeredCommands.size)
assertEquals(1, InternalCommandManager.registeredCommands.size)
assertEquals(1, InternalCommandManager.requiredPrefixCommandMap.size)
} finally {
TestCompositeCommand.unregister()
}
}
@Test
fun testSimpleExecute() = runBlocking {
assertEquals(arrayOf("test").contentToString(), withTesting<Array<String>> {
TestSimpleCommand.execute(sender, "test")
}.contentToString())
}
@Test
fun `test flattenCommandArgs`() {
val result = arrayOf("test", image).flattenCommandComponents().toTypedArray()
assertEquals("test", result[0])
assertSame(image, result[1])
assertEquals(2, result.size)
}
@Test
fun testSimpleArgsSplitting() = runBlocking {
assertEquals(arrayOf("test", "ttt", "tt").contentToString(), withTesting<Array<String>> {
TestSimpleCommand.execute(sender, "test ttt tt".toMessage())
}.contentToString())
}
val image = Image("/f8f1ab55-bf8e-4236-b55e-955848d7069f")
@Test
fun `PlainText and Image args splitting`() = runBlocking {
val result = withTesting<Array<Any>> {
TestSimpleCommand.execute(sender, "test", image, "tt")
}
assertEquals(arrayOf("test", image, "tt").contentToString(), result.contentToString())
assertSame(image, result[1])
}
@Test
fun `test throw Exception`() = runBlocking {
assertEquals(null, sender.executeCommand(""))
}
@Test
fun `executing command by string command`() = runBlocking {
TestCompositeCommand.register()
val result = withTesting<Array<String>> {
assertNotNull(sender.executeCommand("testComposite", "test"))
}
assertEquals("test", result.single())
}
@Test
fun `composite command executing`() = runBlocking {
assertEquals(1, withTesting {
assertNotNull(TestCompositeCommand.execute(sender, "mute 1"))
})
}
@Test
fun `composite sub command resolution conflict`() {
runBlocking {
val composite = object : CompositeCommand(
ConsoleCommandOwner.instance,
"tr"
) {
@SubCommand
fun mute(seconds: Int) {
Testing.ok(1)
}
@SubCommand
fun mute(seconds: Int, arg2: Int) {
Testing.ok(2)
}
}
assertFailsWith<IllegalStateException> {
composite.register()
}
/*
composite.withRegistration {
assertEquals(1, withTesting { execute(sender, "tr", "mute 123") }) // one args, resolves to mute(Int)
assertEquals(2, withTesting { execute(sender, "tr", "mute 123 123") })
}*/
}
}
@Test
fun `composite sub command parsing`() {
runBlocking {
class MyClass(
val value: Int
)
val composite = object : CompositeCommand(
ConsoleCommandOwner.instance,
"test",
overrideContext = CommandParserContext {
add(object : CommandArgParser<MyClass> {
override fun parse(raw: String, sender: CommandSender): MyClass {
return MyClass(raw.toInt())
}
override fun parse(raw: SingleMessage, sender: CommandSender): MyClass {
assertSame(image, raw)
return MyClass(2)
}
})
}
) {
@SubCommand
fun mute(seconds: MyClass) {
Testing.ok(seconds)
}
}
composite.withRegistration {
assertEquals(333, withTesting<MyClass> { execute(sender, "mute 333") }.value)
assertEquals(2, withTesting<MyClass> { execute(sender, "mute", image) }.value)
}
}
}
}

View File

@ -1,36 +0,0 @@
/*
* Copyright 2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
@file:Suppress("unused")
package net.mamoe.mirai.console.command
import net.mamoe.mirai.contact.Member
import net.mamoe.mirai.message.data.Image
import org.junit.jupiter.api.Test
object TestCompositeCommand : CompositeCommand(
TestCommandOwner,
"groupManagement", "grpMgn"
) {
@SubCommand
suspend fun CommandSender.mute(image: Image, target: Member, seconds: Int): Boolean {
target.mute(seconds)
return true
}
}
internal class TestComposite {
@Test
fun testRegister() {
TestCompositeCommand.register()
}
}

View File

@ -0,0 +1,19 @@
/*
* Copyright 2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.console.command
inline fun <T : Command, R> T.withRegistration(block: T.() -> R): R {
this.register()
try {
return block()
} finally {
this.unregister()
}
}

View File

@ -62,9 +62,6 @@ object MiraiConsoleFrontEndPure : MiraiConsoleFrontEnd {
return globalLogger
}
override fun prePushBot(identity: Long) {
}
override fun pushBot(bot: Bot) {
}
@ -80,9 +77,6 @@ object MiraiConsoleFrontEndPure : MiraiConsoleFrontEnd {
return ConsoleUtils.lineReader.readLine("> ")
}
override fun pushBotAdminStatus(identity: Long, admins: List<Long>) {
}
override fun createLoginSolver(): LoginSolver {
return DefaultLoginSolver(
input = suspend {