Browse Source

[CONSOLE] Integration Test (#1741)

* [CONSOLE] Realtime Test Unit

* Rename to IntegrationTest; IDEA Debugging support

* External Plugins support

* Simply redesign
微莹·纤绫 4 years ago
parent
commit
8d6b4b4970

+ 35 - 0
mirai-console/backend/integration-test/README.md

@@ -0,0 +1,35 @@
+# Console - Integration Test
+
+Mirai Console 一体化测试单元 (目前仅内部测试)
+
+---
+
+## 使用 Integration Test Framework
+
+TODO
+
+### 添加一个新测试
+
+#### 创建 Integration Test 测试点
+
+创建一个新的子测试单元并继承 `AbstractTestPoint`
+
+- 在其 `beforeConsoleStartup()` 准备测试环境 (如写入配置文件, etc)
+- 在其 `onConsoleStartSuccessfully()` 检查插件相关行为是否正确
+
+然后在 `MiraiConsoleIntegrationTestLauncher.points` 添加新单元的完整类路径
+
+----
+
+## Mirai Console Internal Testing
+
+### 添加一个新测试 (CONSOLE 内部测试)
+
+在 `test/testpoints` 添加新测试点,
+然后在 [`MiraiConsoleIntegrationTestBootstrap.kt`](test/MiraiConsoleIntegrationTestBootstrap.kt)
+添加相关单元
+
+### 创建配套子插件
+
+在 `testers` 创建新的文件夹即可创建新的配套插件, 可用于测试插件依赖, etc
+

+ 94 - 0
mirai-console/backend/integration-test/build.gradle.kts

@@ -0,0 +1,94 @@
+/*
+ * Copyright 2019-2021 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("UnusedImport")
+
+import java.util.Base64
+
+plugins {
+    kotlin("jvm")
+    kotlin("plugin.serialization")
+    id("java")
+}
+
+version = Versions.console
+description = "Mirai Console Backend Real-Time Testing Unit"
+
+kotlin {
+    explicitApiWarning()
+}
+
+dependencies {
+    api(project(":mirai-core-api"))
+    api(project(":mirai-core-utils"))
+    api(project(":mirai-console-compiler-annotations"))
+    api(project(":mirai-console"))
+    api(project(":mirai-console-terminal"))
+
+    api(`kotlin-stdlib-jdk8`)
+    api(`kotlinx-atomicfu-jvm`)
+    api(`kotlinx-coroutines-core-jvm`)
+    api(`kotlinx-serialization-core-jvm`)
+    api(`kotlinx-serialization-json-jvm`)
+    api(`kotlin-reflect`)
+    api(`kotlin-test-junit5`)
+
+
+    api(`yamlkt-jvm`)
+    api(`jetbrains-annotations`)
+    api(`caller-finder`)
+    api(`kotlinx-coroutines-jdk8`)
+
+
+    val asmVersion = Versions.asm
+    fun asm(module: String) = "org.ow2.asm:asm-$module:$asmVersion"
+
+    api(asm("tree"))
+    api(asm("util"))
+    api(asm("commons"))
+
+}
+
+val subplugins = mutableListOf<TaskProvider<Jar>>()
+
+val mcit_test = tasks.named<Test>("test")
+mcit_test.configure {
+    val test0 = this
+    doFirst {
+        // For IDEA Debugging
+        @Suppress("UNNECESSARY_NOT_NULL_ASSERTION")
+        val extArgs = test0.jvmArgs!!.asSequence().map { extArg ->
+            Base64.getEncoder().encodeToString(extArg.toByteArray())
+        }.joinToString(",")
+        test0.jvmArgs = mutableListOf()
+        test0.environment("IT_ARGS", extArgs)
+
+        // For plugins coping
+        val jars = subplugins.asSequence()
+            .map { it.get() }
+            .flatMap { it.outputs.files.files.asSequence() }
+            .toList()
+
+        test0.environment("IT_PLUGINS", jars.size)
+        jars.forEachIndexed { index, jar ->
+            test0.environment("IT_PLUGINS_$index", jar.absolutePath)
+        }
+
+    }
+}
+
+rootProject.allprojects {
+    if (project.path.removePrefix(":").startsWith("mirai-console.integration-test.tp.")) {
+        project.afterEvaluate {
+            val tk = tasks.named<Jar>("jar")
+            subplugins.add(tk)
+            mcit_test.configure { dependsOn(tk) }
+        }
+    }
+}

+ 45 - 0
mirai-console/backend/integration-test/src/AbstractTestPoint.kt

@@ -0,0 +1,45 @@
+/*
+ * Copyright 2019-2021 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/dev/LICENSE
+ */
+
+package net.mamoe.console.integrationtest
+
+/**
+ * IntegrationTest 测试单元
+ *
+ * 每个被注册的单元都会在 console 启动的各个阶段调用相关的函数, 可以在相关函数执行测试代码
+ *
+ * ## 注册单元
+ *
+ * 每个单元都需要被注册, 即被添加进 [MiraiConsoleIntegrationTestLauncher.points]
+ *
+ * @see MiraiConsoleIntegrationTestLauncher
+ * @see AbstractTestPointAsPlugin
+ */
+public abstract class AbstractTestPoint {
+    /**
+     * 本函数会在 console 启动前调用, 可以在此处进行环境配置
+     */
+    protected open fun beforeConsoleStartup() {}
+
+    /**
+     * 本函数会在 console 启动成功后立即调用, 可进行环境检查, 命令执行测试, 或更多
+     */
+    protected open fun onConsoleStartSuccessfully() {}
+
+    // access
+    internal companion object {
+        internal fun AbstractTestPoint.internalOSS() {
+            onConsoleStartSuccessfully()
+        }
+
+        internal fun AbstractTestPoint.internalBCS() {
+            beforeConsoleStartup()
+        }
+    }
+}

+ 62 - 0
mirai-console/backend/integration-test/src/AbstractTestPointAsPlugin.kt

@@ -0,0 +1,62 @@
+/*
+ * Copyright 2019-2021 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/dev/LICENSE
+ */
+
+package net.mamoe.console.integrationtest
+
+import net.mamoe.mirai.console.extension.PluginComponentStorage
+import net.mamoe.mirai.console.plugin.jvm.JvmPluginDescription
+import net.mamoe.mirai.console.plugin.jvm.KotlinPlugin
+
+/**
+ * IntegrationTest 测试单元 (Plugin mode)
+ *
+ * 该单元除了拥有 [AbstractTestPoint] 具有的功能之外, 还可以直接模拟一个插件的行为.
+ *
+ * 在此单元里, 可以像写正常的 console 插件一样在此写测试时插件
+ */
+public abstract class AbstractTestPointAsPlugin : AbstractTestPoint() {
+    protected abstract fun newPluginDescription(): JvmPluginDescription
+
+    protected open fun KotlinPlugin.onInit() {}
+    protected open fun KotlinPlugin.onLoad0(storage: PluginComponentStorage) {}
+    protected open fun KotlinPlugin.onEnable0() {}
+    protected open fun KotlinPlugin.onDisable0() {}
+
+
+
+    @Suppress("unused")
+    @PublishedApi
+    internal abstract class TestPointPluginImpl(
+        private val impl: AbstractTestPointAsPlugin
+    ) : KotlinPlugin(impl.newPluginDescription()) {
+
+        init {
+            impl.apply { onInit() }
+        }
+
+        @PublishedApi
+        internal constructor(
+            impl: Class<out AbstractTestPointAsPlugin>
+        ) : this(impl.kotlin.objectInstance ?: impl.newInstance())
+
+        override fun onDisable() {
+            impl.apply { onDisable0() }
+        }
+
+        override fun onEnable() {
+            impl.apply { onEnable0() }
+        }
+
+        override fun PluginComponentStorage.onLoad() {
+            impl.apply { onLoad0(this@onLoad) }
+        }
+    }
+
+}
+

+ 148 - 0
mirai-console/backend/integration-test/src/IntegrationTestBootstrap.kt

@@ -0,0 +1,148 @@
+/*
+ * Copyright 2019-2021 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/dev/LICENSE
+ */
+
+@file:JvmName("IntegrationTestBootstrap")
+
+package net.mamoe.console.integrationtest
+
+import kotlinx.coroutines.cancelAndJoin
+import kotlinx.coroutines.runBlocking
+import net.mamoe.console.integrationtest.AbstractTestPoint.Companion.internalBCS
+import net.mamoe.console.integrationtest.AbstractTestPoint.Companion.internalOSS
+import net.mamoe.mirai.console.MiraiConsole
+import net.mamoe.mirai.console.terminal.ConsoleTerminalExperimentalApi
+import net.mamoe.mirai.console.terminal.ConsoleTerminalSettings
+import net.mamoe.mirai.console.terminal.MiraiConsoleTerminalLoader
+import net.mamoe.mirai.utils.cast
+import org.objectweb.asm.ClassWriter
+import org.objectweb.asm.Opcodes
+import org.objectweb.asm.Type
+import java.io.File
+import java.io.FileOutputStream
+import java.util.zip.ZipEntry
+import java.util.zip.ZipOutputStream
+import kotlin.system.exitProcess
+
+/**
+ * 入口点为 /test/MiraiConsoleIntegrationTestBootstrap.kt 并非此函数(文件),
+ * 不要直接执行此函数
+ */
+@OptIn(ConsoleTerminalExperimentalApi::class)
+@PublishedApi
+internal fun main() {
+    // PRE CHECK
+    kotlin.run {
+        if (!System.getenv("MIRAI_CONSOLE_INTEGRATION_TEST").orEmpty().toBoolean()) {
+            error("Don't launch IntegrationTestBootstrap directly. See /test/MiraiConsoleIntegrationTestBootstrap.kt")
+        }
+    }
+    // @context: env.testunit = true
+    // @context: env.inJUnitProcess = false
+    // @context: env.exitProcessSafety = true
+    // @context: process.type = sandbox
+    // @context: process.cwd = /mirai-console/backend/build/rttu
+    // @context: process.timeout = 5min
+
+    ConsoleTerminalSettings.setupAnsi = false
+    ConsoleTerminalSettings.noConsole = true
+
+    val testUnits: List<AbstractTestPoint> = readStringListFromEnv("IT_POINTS").asSequence()
+        .onEach { println("[MCIT] Loading test point: $it") }
+        .map { Class.forName(it) }
+        .map { it.kotlin.objectInstance ?: it.newInstance() }
+        .map { it.cast<AbstractTestPoint>() }
+        .toList()
+
+    File("plugins").mkdirs()
+    prepareConsole()
+
+    testUnits.forEach { (it as? AbstractTestPointAsPlugin)?.generatePluginJar() }
+    testUnits.forEach { it.internalBCS() }
+
+    MiraiConsoleTerminalLoader.startAsDaemon()
+
+    if (!MiraiConsole.isActive) {
+        error("Failed to start console")
+    }
+
+    // I/main: mirai-console started successfully.
+
+    testUnits.forEach { it.internalOSS() }
+
+    runBlocking {
+        MiraiConsole.job.cancelAndJoin()
+    }
+    exitProcess(0)
+}
+
+private fun File.mkparents(): File = apply { parentFile?.mkdirs() }
+private fun prepareConsole() {
+    File("config/Console/Logger.yml").mkparents().writeText(
+        """
+defaultPriority: ALL
+loggers: 
+  Bot: ALL
+"""
+    )
+
+    readStringListFromEnv("IT_PLUGINS").forEach { path ->
+        val jarFile = File(path)
+        val target = File("plugins/${jarFile.name}").mkparents()
+        jarFile.copyTo(target, overwrite = true)
+        println("[MCIT] Copied external plugin: $jarFile")
+    }
+}
+
+private fun AbstractTestPointAsPlugin.generatePluginJar() {
+    val simpleName = this.javaClass.simpleName
+    val point = this
+    val jarFile = File("plugins").resolve("$simpleName.jar")
+    // PluginMainPoint: net.mamoe.console.integrationtestAbstractTestPointAsPlugin$TestPointPluginImpl
+    jarFile.mkparents()
+    ZipOutputStream(
+        FileOutputStream(jarFile).buffered()
+    ).use { zipOutputStream ->
+
+        // META-INF/services/net.mamoe.mirai.console.plugin.jvm.JvmPlugin
+        zipOutputStream.putNextEntry(
+            ZipEntry(
+                "META-INF/services/net.mamoe.mirai.console.plugin.jvm.JvmPlugin"
+            )
+        )
+        val delegateClassName = "net.mamoe.console.integrationtest.tpd.$simpleName"
+        zipOutputStream.write(delegateClassName.toByteArray())
+
+        // MainClass
+        val internalClassName = delegateClassName.replace('.', '/')
+        zipOutputStream.putNextEntry(ZipEntry("$internalClassName.class"))
+        val classWriter = ClassWriter(ClassWriter.COMPUTE_MAXS)
+        val superName = "net/mamoe/console/integrationtest/AbstractTestPointAsPlugin\$TestPointPluginImpl"
+        classWriter.visit(
+            Opcodes.V1_8,
+            Opcodes.ACC_PUBLIC,
+            internalClassName,
+            null,
+            superName,
+            null
+        )
+        classWriter.visitMethod(
+            Opcodes.ACC_PUBLIC,
+            "<init>", "()V", null, null
+        )!!.let { initMethod ->
+            initMethod.visitVarInsn(Opcodes.ALOAD, 0)
+            initMethod.visitLdcInsn(Type.getType(point.javaClass))
+            initMethod.visitMethodInsn(Opcodes.INVOKESPECIAL, superName, "<init>", "(Ljava/lang/Class;)V", false)
+            initMethod.visitInsn(Opcodes.RETURN)
+            initMethod.visitMaxs(0, 0)
+            initMethod.visitEnd()
+        }
+
+        zipOutputStream.write(classWriter.toByteArray())
+    }
+}

+ 128 - 0
mirai-console/backend/integration-test/src/MiraiConsoleIntegrationTestLauncher.kt

@@ -0,0 +1,128 @@
+/*
+ * Copyright 2019-2021 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/dev/LICENSE
+ */
+
+package net.mamoe.console.integrationtest
+
+import net.mamoe.mirai.utils.lateinitMutableProperty
+import java.io.File
+import java.io.OutputStream
+import java.io.PrintStream
+import java.lang.management.ManagementFactory
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicBoolean
+import kotlin.concurrent.thread
+
+// TODO: 不完整, 还无法完全公开, 目前仅允许 console 内部使用
+/**
+ * MiraiConsoleIntegrationTest 启动器
+ */
+public class MiraiConsoleIntegrationTestLauncher {
+    /** java.exe 路径 */
+    public var javaexec: String by lateinitMutableProperty { findJavaExec() }
+
+    /**
+     * 测试环境运行目录, **每次启动前会直接删除该文件夹的内容**(IMPORTANT)
+     */
+    public var workingDir: File = File("mirai-console-integration-test")
+
+    /** 额外 JVM 参数 */
+    public var vmoptions: MutableList<String> = mutableListOf()
+
+    /** 额外环境变量 */
+    public var extraEnvironment: MutableMap<String, String> = mutableMapOf()
+
+    /** 类路径, 需要包含 MiraiConsoleIntegrationTest Framework */
+    public var classpath: String by lateinitMutableProperty { ManagementFactory.getRuntimeMXBean().classPath }
+
+    /** 标准输出重定向位置 */
+    public var output: OutputStream = System.out
+    /** 标准错误重定向位置 */
+    public var error: OutputStream = System.err
+    /** [MiraiConsoleIntegrationTestLauncher] 启动日志的输出 */
+    public var log: PrintStream = System.out
+
+    /** 测试单元完整类名, 需要可以在 [classpath] 中找到 */
+    public var points: MutableCollection<String> = mutableListOf()
+    /** 测试环境的额外插件, 为文件路径, 相对于 [workingDir] */
+    public var plugins: MutableCollection<String> = mutableListOf()
+
+    public fun launch() {
+        workingDir.deleteRecursively()
+        workingDir.mkdirs()
+        val isDebugging = vmoptions.any { it.startsWith("-agentlib:") }
+
+        val builder = ProcessBuilder(
+            javaexec,
+            *vmoptions.toTypedArray(),
+            "-cp", classpath,
+            "net.mamoe.console.integrationtest.IntegrationTestBootstrap",
+        )
+            .directory(workingDir)
+        // .inheritIO() // No output in idea
+        val env = builder.environment()
+        env.putAll(extraEnvironment)
+        env["MIRAI_CONSOLE_INTEGRATION_TEST"] = "true"
+        saveStringListToEnv("IT_PLUGINS", plugins, env)
+        saveStringListToEnv("IT_POINTS", points, env)
+
+        log.println("[MCIT] Launching IntegrationTest")
+        log.println("[MCIT]    `- Arguments: ${builder.command().joinToString(" ")}")
+        log.println("[MCIT]    `- Directory: ${builder.directory().absoluteFile}")
+        log.println("[MCIT]    `- Debugging: $isDebugging")
+        if (isDebugging) {
+            log.println("[MCIT] Running in debug mode. Watchdog thread will not start")
+        }
+
+        val process = builder.start()
+
+        val timedOut = AtomicBoolean(false)
+        val watchdog = thread {
+            if (isDebugging) return@thread
+            try {
+                Thread.sleep(TimeUnit.MINUTES.toMillis(5))
+                timedOut.set(true)
+                process.destroyForcibly()
+            } catch (ignored: InterruptedException) {
+            }
+        }
+
+        thread { process.inputStream.copyTo(output) }
+        thread { process.errorStream.copyTo(error) }
+
+        val rsp = process.waitFor()
+        if (timedOut.get()) {
+            error("Mirai console daemon timed out")
+        }
+        watchdog.interrupt()
+        if (rsp != 0) error("Rsp $rsp")
+    }
+}
+
+private fun findJavaExec(): String {
+    findJavaExec0()?.let { return it.absolutePath }
+    System.err.println("[MCIT] WARNING: Unable to determine the current runtime executable path.")
+    System.err.println("[MCIT] WARNING: Using default executable to launch test unit")
+    return "java"
+}
+
+private fun findJavaExec0(): File? {
+    val ext = if ("windows" in System.getProperty("os.name").lowercase()) {
+        ".exe"
+    } else ""
+
+    val javaHome = File(System.getProperty("java.home"))
+    javaHome.resolve("bin/java$ext").takeIf { it.exists() }?.let { return it }
+    javaHome.resolve("java$ext").takeIf { it.exists() }?.let { return it }
+
+
+    javaHome.resolve("jre/bin/java$ext").takeIf { it.exists() }?.let { return it }
+    javaHome.resolve("jre/java$ext").takeIf { it.exists() }?.let { return it }
+
+    return null
+}

+ 26 - 0
mirai-console/backend/integration-test/src/utils.kt

@@ -0,0 +1,26 @@
+/*
+ * Copyright 2019-2021 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/dev/LICENSE
+ */
+
+package net.mamoe.console.integrationtest
+
+internal fun readStringListFromEnv(key: String): MutableList<String> {
+    val size = System.getenv(key)!!.toInt()
+    val rsp = mutableListOf<String>()
+    for (i in 0 until size) {
+        rsp.add(System.getenv("${key}_$i")!!)
+    }
+    return rsp
+}
+
+internal fun saveStringListToEnv(key: String, value: Collection<String>, env: MutableMap<String, String>) {
+    env[key] = value.size.toString()
+    value.forEachIndexed { index, v ->
+        env["${key}_$index"] = v
+    }
+}

+ 52 - 0
mirai-console/backend/integration-test/test/MiraiConsoleIntegrationTestBootstrap.kt

@@ -0,0 +1,52 @@
+/*
+ * Copyright 2019-2021 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/dev/LICENSE
+ */
+
+package net.mamoe.console.integrationtest
+
+import net.mamoe.console.integrationtest.testpoints.DoNothingPoint
+import net.mamoe.console.integrationtest.testpoints.MCITBSelfAssertions
+import org.junit.jupiter.api.Test
+import java.io.File
+import java.lang.management.ManagementFactory
+import java.util.*
+import kotlin.reflect.KClass
+
+
+class MiraiConsoleIntegrationTestBootstrap {
+    @Test
+    fun bootstrap() {
+        /*
+        implementation note:
+        不使用 @TempDir 是为了保存最后一次失败快照, 便于 debug
+         */
+        val workingDir = File("build/IntegrationTest") // mirai-console/backend/integration-test/build/IntegrationTest
+        val launcher = MiraiConsoleIntegrationTestLauncher()
+        launcher.workingDir = workingDir
+        launcher.plugins = readStringListFromEnv("IT_PLUGINS")
+        launcher.points = listOf<Any>(
+            DoNothingPoint,
+            MCITBSelfAssertions,
+        ).asSequence().map { v ->
+            when (v) {
+                is Class<*> -> v
+                is KClass<*> -> v.java
+                else -> v.javaClass
+            }
+        }.map { it.name }.toMutableList()
+        launcher.vmoptions = mutableListOf(
+            *ManagementFactory.getRuntimeMXBean().inputArguments.filterNot {
+                it.startsWith("-Djava.security.manager=")
+            }.toTypedArray(),
+            *System.getenv("IT_ARGS")!!.splitToSequence(",").map {
+                Base64.getDecoder().decode(it).decodeToString()
+            }.filter { it.isNotEmpty() }.toList().toTypedArray()
+        )
+        launcher.launch()
+    }
+}

+ 44 - 0
mirai-console/backend/integration-test/test/testpoints/DoNothingPoint.kt

@@ -0,0 +1,44 @@
+/*
+ * Copyright 2019-2021 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/dev/LICENSE
+ */
+
+package net.mamoe.console.integrationtest.testpoints
+
+import net.mamoe.console.integrationtest.AbstractTestPointAsPlugin
+import net.mamoe.mirai.console.plugin.jvm.JvmPluginDescription
+import net.mamoe.mirai.console.plugin.jvm.KotlinPlugin
+import net.mamoe.mirai.utils.info
+
+/*
+DoNothingPoint: Example
+ */
+internal object DoNothingPoint : AbstractTestPointAsPlugin() {
+    var enableCalled = false
+    override fun newPluginDescription(): JvmPluginDescription {
+        return JvmPluginDescription(
+            id = "net.mamoe.testpoint.do-nothing",
+            version = "1.1.0",
+            name = "DoNothing",
+        )
+    }
+
+    override fun KotlinPlugin.onEnable0() {
+        logger.info { "DoNothing.onEnable()  called" }
+        enableCalled = true
+    }
+
+    override fun KotlinPlugin.onDisable0() {
+        logger.info { "DoNothing.onDisable() called" }
+    }
+
+    override fun onConsoleStartSuccessfully() {
+        assert(enableCalled) {
+            "DoNothing.onEnable() not called."
+        }
+    }
+}

+ 50 - 0
mirai-console/backend/integration-test/test/testpoints/MCITBSelfAssertions.kt

@@ -0,0 +1,50 @@
+/*
+ * Copyright 2019-2021 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/dev/LICENSE
+ */
+
+package net.mamoe.console.integrationtest.testpoints
+
+import net.mamoe.console.integrationtest.AbstractTestPointAsPlugin
+import net.mamoe.mirai.console.plugin.PluginManager
+import net.mamoe.mirai.console.plugin.PluginManager.INSTANCE.description
+import net.mamoe.mirai.console.plugin.jvm.JvmPluginDescription
+import net.mamoe.mirai.console.plugin.jvm.KotlinPlugin
+import kotlin.test.*
+
+/*
+MCITBSelfAssertions: 用于检查 Integration Test 可以正常加载 AbstractTestPointAsPlugin 与 外部测试插件
+ */
+internal object MCITBSelfAssertions : AbstractTestPointAsPlugin() {
+    override fun newPluginDescription(): JvmPluginDescription {
+        return JvmPluginDescription(
+            id = "net.mamoe.testpoint.mirai-console-self-assertions",
+            version = "1.0.0",
+            name = "MCITBSelfAssertions",
+        )
+    }
+
+    var called = false
+
+    override fun KotlinPlugin.onEnable0() {
+        called = true
+        assertFails { error("") }
+        assertTrue { true }
+        assertFalse { false }
+        assertFailsWith<InternalError> { throw InternalError("") }
+        assertEquals("", "")
+        assertSame(this, this)
+    }
+
+    override fun onConsoleStartSuccessfully() {
+        assertTrue(called, "Mirai Console IntegrationTestBootstrap Internal Error")
+
+        assertTrue("MCITSelfTestPlugin not found") {
+            PluginManager.plugins.any { it.description.id == "net.mamoe.tester.mirai-console-self-test" }
+        }
+    }
+}

+ 1 - 0
mirai-console/backend/integration-test/testers/.gitignore

@@ -0,0 +1 @@
+build.gradle.kts

+ 1 - 0
mirai-console/backend/integration-test/testers/MCITSelfTestPlugin/resources/META-INF/services/net.mamoe.mirai.console.plugin.jvm.JvmPlugin

@@ -0,0 +1 @@
+net.mamoe.console.integrationtest.ep.mcitselftest.MCITSelfTestPlugin

+ 33 - 0
mirai-console/backend/integration-test/testers/MCITSelfTestPlugin/src/MCITSelfTestPlugin.kt

@@ -0,0 +1,33 @@
+/*
+ * Copyright 2019-2021 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/dev/LICENSE
+ */
+
+package net.mamoe.console.integrationtest.ep.mcitselftest
+
+import net.mamoe.mirai.console.plugin.jvm.JvmPluginDescription
+import net.mamoe.mirai.console.plugin.jvm.KotlinPlugin
+import net.mamoe.mirai.utils.info
+import kotlin.test.assertTrue
+
+/*
+MCITSelfTestPlugin: 用于测试 Integration-test 可正常加载
+@see /test/testpoints/MCITBSelfAssertions
+ */
+public object MCITSelfTestPlugin : KotlinPlugin(
+    JvmPluginDescription(
+        id = "net.mamoe.tester.mirai-console-self-test",
+        version = "1.0.0",
+        name = "MCITSelfTestPlugin",
+    )
+) {
+    override fun onEnable() {
+        logger.info { "MCITSelfTestPlugin.onEnable() called" }
+
+        assertTrue { true }
+    }
+}

+ 13 - 0
mirai-console/backend/integration-test/testers/README.md

@@ -0,0 +1,13 @@
+# Integration Test - Sub Testers
+
+Integration Test 的测试插件, 放置在本文件夹内的全部插件均为 console 内部测试用插件
+
+如果您不是正在修改 mirai-console, 则不需要阅读此文件及此模块
+
+---
+
+创建新测试插件只需要在本文件夹创建新的目录, 然后重载 (Reimport gradle projects)
+
+如果需要添加新的依赖, 请在 [`IntegrationTest/build.gradle.kts`](../build.gradle.kts) 添加相关依赖 (使用 `testApi`) 并标注哪个测试框架使用此依赖, 为何使用此依赖
+
+如果需要自定义 `build.gradle.kts`, 请在 IDEA 右键 `build.gradle.kts` 并选择 `Git > Add File`

+ 26 - 0
mirai-console/backend/integration-test/testers/tester.template.gradle.kts

@@ -0,0 +1,26 @@
+/*
+ * Copyright 2019-2021 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("UnusedImport")
+
+plugins {
+    kotlin("jvm")
+    kotlin("plugin.serialization")
+    id("java")
+}
+
+version = "0.0.0"
+
+kotlin {
+    explicitApiWarning()
+}
+
+dependencies {
+    api(project(":mirai-console.integration-test"))
+}

+ 28 - 0
settings.gradle.kts

@@ -52,6 +52,34 @@ includeConsoleProject(":mirai-console-compiler-annotations", "tools/compiler-ann
 includeConsoleProject(":mirai-console", "backend/mirai-console")
 includeConsoleProject(":mirai-console.codegen", "backend/codegen")
 includeConsoleProject(":mirai-console-terminal", "frontend/mirai-console-terminal")
+
+// region mirai-console.integration-test
+includeConsoleProject(":mirai-console.integration-test", "backend/integration-test")
+
+val consoleIntegrationTestSubPluginBuildGradleKtsTemplate by lazy {
+    rootProject.projectDir
+        .resolve("mirai-console/backend/integration-test/testers")
+        .resolve("tester.template.gradle.kts")
+        .readText()
+}
+
+@Suppress("SimpleRedundantLet")
+fun includeConsoleITPlugin(path: File) {
+    path.resolve("build.gradle.kts").takeIf { !it.isFile }?.let { initScript ->
+        initScript.writeText(consoleIntegrationTestSubPluginBuildGradleKtsTemplate)
+    }
+
+    val projectPath = ":mirai-console.integration-test.tp.${path.name}"
+    include(projectPath)
+    project(projectPath).projectDir = path
+}
+rootProject.projectDir
+    .resolve("mirai-console/backend/integration-test/testers")
+    .listFiles()?.asSequence().orEmpty()
+    .filter { it.isDirectory }
+    .forEach { includeConsoleITPlugin(it) }
+// endregion
+
 includeConsoleProject(":mirai-console-compiler-common", "tools/compiler-common")
 includeConsoleProject(":mirai-console-intellij", "tools/intellij-plugin")
 includeConsoleProject(":mirai-console-gradle", "tools/gradle-plugin")