22 Commits

Author SHA1 Message Date
efbb57f1f7 release: 发布 0.3.0 版本. 2022-05-18 15:58:22 +08:00
5e18149640 feat(config): 支持限定 Maven 仓库构件类型.
增加 Maven 仓库配置, 支持限定仓库可获取的构件发布类型(发布或快照).
此改动有利于用户增加仓库约束, 防止意外使用错误的扩展包版本.
2022-05-18 15:57:49 +08:00
0a5313e94a fix(extension): 修复 Maven 仓库扩展搜索器无法从第三方仓库获取扩展的问题.
由于在加载仓库配置时, 未设置仓库 Id, 导致 Aether 将仓库排除, 进而导致无法通过第三方仓库获取插件.
改动后, 将在未配置仓库 Id 的情况下, 为其生成一个 Id 名称.
2022-05-18 15:40:33 +08:00
a0afde52ac fix(launch): 修复 Maven 本地仓库文件夹未初始化的问题.
由于 Maven 本地仓库文件夹未初始化, 将导致启动时发生错误, 现已修复该问题.
2022-05-17 19:59:04 +08:00
ef37f3b2d7 fix(bot): 修复因机器人命令列表为空而导致命令列表自动更新报错的问题.
当机器人因扩展加载失败时, 将无法正常执行命令更新操作, 故添加空命令检查以避免该问题.
2022-05-17 19:56:42 +08:00
6e59a9a5ac build(publish): 增加构件签名过程.
增加构件 GPG 签名, 可保证构件未被修改, 增加构件可信度.
2022-05-17 19:26:26 +08:00
a44732a7f6 build: 将 Maven 发布仓库从 Github Repo 改为自建 Nexus 仓库.
由于 Github 自建仓库在 SNAPSHOT 版本上存在问题, 故修改发布配置以转移到自建的 Nexus 仓库.
2022-05-17 19:03:58 +08:00
95ad251826 refactor(utils): 移除不再使用的方法.
当初开发 Maven 仓库搜索器时意外提交的, 已经可以移除了.
2022-05-07 01:43:15 +08:00
8174f2a3a2 refactor(bot): 修正提示信息错误, 移除空父类方法调用.
修正了运行指标信息中的拼写错误, 移除对无操作父类方法的调用, 这么做可以明确表明只有子类实现了操作.
2022-05-07 01:37:21 +08:00
478480014a perf(utils): 优化自动释放钩子的资源引用.
原本自动释放钩子对资源的引用, 可能会出现资源已经被关闭, 但仍然无法被 GC 回收的问题.
此次改动, 将会让钩子在关闭资源后, 将资源从列表中移除.
虽然, 自动释放钩子设计上仅会被 System.exit 动作触发, 但保险起见还是加上这个改动.
2022-05-05 16:52:29 +08:00
830f05c90a refactor(utils): 加强 getPriority 方法的优先值判断.
加强优先级判断, 有利于后续使用时防止出现意外情况的问题.
顺便补充一手单元测试.
2022-05-05 16:13:48 +08:00
8be0978783 refactor: 更改 AppConfig 的获取方式, 以便于编写测试用例.
通过 Const 单例对象获取配置信息不利于编写测试用例, 所以改为利用参数的默认值来获取 Const 的 config 对象.

Issue #5
2022-05-04 23:55:21 +08:00
ce613787f6 fix: 修正方法参数使用错误的问题.
MavenRepositoryConfig 的 toRemoteRepository 方法使用了参数默认值, 可能会导致意外使用常量的情况,
故移除 MavenRepositoryConfig.toRemoteRepository 的参数默认值.

Pull Request #6
2022-05-04 23:07:45 +08:00
2389d082f4 test(config): 优化对 defaultInitializer 方法的单元测试.
将 defaultInitializer 方法的反射获取次数减少为一次, 并在测试结束后恢复访问权设置.
2022-05-04 22:36:35 +08:00
27f54c3c36 test(config): 补充一部分 AppPaths 的单元测试项目.
补充了针对 AppPaths.defaultInitializer() 和 AppPaths.DATA_ROOT 的单元测试项.
其他的有待补充.
2022-05-04 02:00:01 +08:00
7b985ce325 refactor: 将十六进制转换代码迁移到 Kotlin.
将 ByteUtils 的实现改用 Kotlin 代码做, 移除 ByteUtils.
另外, 本次修改同时修正了方法名错误的问题(hax 改成 hex), 并补充了单元测试.
2022-05-04 00:38:30 +08:00
77b7a7cd08 feat(launch): 对配置中没有启用任何机器人的情况输出警告.
增加对没有启用任何机器人时候的一个警告信息, 以防止被误认为无响应退出.
2022-05-02 02:20:19 +08:00
e8b746b3f8 feat(config): 第一次运行将提醒用户更改配置文件.
之前忘记添加这个提醒了, 首次运行的时候, `config.json` 和 `bot.json` 是不存在的, 所以根据这两个文件的存在与否, 来判定并提醒用户更改配置文件.
2022-05-02 02:18:23 +08:00
d24572a4f3 refactor(config): 修改 AppConfig 的获取方式, 便于编写测试用例.
通过 Const 单例对象获取配置信息不利于编写测试用例, 所以改为利用参数的默认值来获取 Const 的 config 对象.

Issue #5
2022-05-01 23:54:22 +08:00
f11290c73d feat: 可以覆盖 Maven 中央仓库配置.
原本设计是无论配置文件中是否带有 Maven 中央仓库, 都会添加 Maven 中央仓库进去, 这样可能会覆盖用户的仓库配置.
新改动将检查配置中是否添加了 Maven 中央仓库配置来决定是否补充 Maven 中央仓库.
2022-05-01 23:09:44 +08:00
1f2ab0f9b1 fix(extension): 修复搜索器错误日志不包括异常信息.
意外漏掉了这个错误信息, 目前已补充, 以方便寻找问题.
2022-05-01 00:09:28 +08:00
d14ef9de36 fix: 修复Maven 本地仓库文件夹未初始化的问题.
当本地仓库文件夹未初始化时, 将导致文件写入失败, 已修复该问题.
2022-04-30 21:12:36 +08:00
14 changed files with 381 additions and 102 deletions

View File

@ -7,5 +7,5 @@ allprojects {
}
group = "net.lamgc"
version = "0.2.1"
version = "0.3.0"
}

View File

@ -35,6 +35,7 @@ dependencies {
implementation("io.prometheus:simpleclient_httpserver:0.15.0")
testImplementation(kotlin("test"))
testImplementation("io.mockk:mockk:1.12.3")
}
tasks.test {

View File

@ -1,17 +0,0 @@
package net.lamgc.scalabot.util;
final class ByteUtils {
private ByteUtils() {
}
public static String bytesToHexString(byte[] bytes) {
StringBuilder builder = new StringBuilder();
for (byte aByte : bytes) {
String hexBit = Integer.toHexString(aByte & 0xFF);
builder.append(hexBit.length() == 1 ? "0" + hexBit : hexBit);
}
return builder.toString();
}
}

View File

@ -13,12 +13,15 @@ import org.eclipse.aether.artifact.Artifact
import org.eclipse.aether.repository.Authentication
import org.eclipse.aether.repository.Proxy
import org.eclipse.aether.repository.RemoteRepository
import org.eclipse.aether.repository.RepositoryPolicy
import org.telegram.telegrambots.bots.DefaultBotOptions
import org.telegram.telegrambots.meta.ApiConstants
import java.io.File
import java.net.URL
import java.nio.charset.StandardCharsets
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
import kotlin.system.exitProcess
private val log = KotlinLogging.logger { }
@ -99,20 +102,40 @@ internal data class MetricsConfig(
* @property layout 仓库布局版本, Maven 2 及以上使用 `default`, Maven 1 使用 `legacy`.
*/
internal data class MavenRepositoryConfig(
val id: String? = null,
val url: URL,
val proxy: Proxy? = Proxy("http", "127.0.0.1", 1080),
val layout: String = "default",
val enableReleases: Boolean = true,
val enableSnapshots: Boolean = true,
// 可能要设计个 type 来判断解析成什么类型的 Authentication.
val authentication: Authentication? = null
) {
fun toRemoteRepository(): RemoteRepository {
val builder = RemoteRepository.Builder(null, checkRepositoryLayout(layout), url.toString())
fun toRemoteRepository(proxyConfig: ProxyConfig): RemoteRepository {
val builder =
RemoteRepository.Builder(id ?: createDefaultRepositoryId(), checkRepositoryLayout(layout), url.toString())
if (proxy != null) {
builder.setProxy(proxy)
} else if (Const.config.proxy.type == DefaultBotOptions.ProxyType.HTTP) {
builder.setProxy(Const.config.proxy.toAetherProxy())
} else if (proxyConfig.type == DefaultBotOptions.ProxyType.HTTP) {
builder.setProxy(proxyConfig.toAetherProxy())
}
builder.setReleasePolicy(
RepositoryPolicy(
enableReleases,
RepositoryPolicy.UPDATE_POLICY_NEVER,
RepositoryPolicy.CHECKSUM_POLICY_FAIL
)
)
builder.setSnapshotPolicy(
RepositoryPolicy(
enableSnapshots,
RepositoryPolicy.UPDATE_POLICY_ALWAYS,
RepositoryPolicy.CHECKSUM_POLICY_WARN
)
)
return builder.build()
}
@ -124,6 +147,13 @@ internal data class MavenRepositoryConfig(
}
return type
}
private val repoNumber = AtomicInteger(1)
fun createDefaultRepositoryId(): String {
return "Repository-${repoNumber.getAndIncrement()}"
}
}
}
@ -179,7 +209,8 @@ internal enum class AppPaths(
AppConfig(
mavenRepositories = listOf(
MavenRepositoryConfig(
URL(MavenRepositoryExtensionFinder.MAVEN_CENTRAL_URL)
id = "central",
url = URL(MavenRepositoryExtensionFinder.MAVEN_CENTRAL_URL)
)
)
), it
@ -268,9 +299,17 @@ private fun AppPaths.defaultInitializer() {
}
internal fun initialFiles() {
val configFilesNotInitialized = !AppPaths.DEFAULT_CONFIG_APPLICATION.file.exists()
&& !AppPaths.DEFAULT_CONFIG_BOT.file.exists()
for (path in AppPaths.values()) {
path.initial()
}
if (configFilesNotInitialized) {
log.warn { "配置文件已初始化, 请根据需要修改配置文件后重新启动本程序." }
exitProcess(1)
}
}
private object GsonConst {

View File

@ -9,6 +9,14 @@ import org.telegram.telegrambots.bots.DefaultBotOptions
import org.telegram.telegrambots.meta.TelegramBotsApi
import org.telegram.telegrambots.meta.generics.BotSession
import org.telegram.telegrambots.updatesreceivers.DefaultBotSession
import java.io.File
import java.io.IOException
import java.nio.file.attribute.PosixFilePermission
import java.nio.file.attribute.PosixFilePermissions
import kotlin.io.path.createDirectories
import kotlin.io.path.exists
import kotlin.io.path.isReadable
import kotlin.io.path.isWritable
import kotlin.system.exitProcess
private val log = KotlinLogging.logger { }
@ -21,9 +29,7 @@ fun main(args: Array<String>): Unit = runBlocking {
val launcher = Launcher()
.registerShutdownHook()
if (Const.config.metrics.enable) {
startMetricsServer()
}
startMetricsServer()
if (!launcher.launch()) {
exitProcess(1)
}
@ -33,11 +39,16 @@ fun main(args: Array<String>): Unit = runBlocking {
* 启动运行指标服务器.
* 使用 Prometheus 指标格式.
*/
fun startMetricsServer() {
internal fun startMetricsServer(config: MetricsConfig = Const.config.metrics) {
if (!config.enable) {
log.debug { "运行指标服务器已禁用." }
return
}
val builder = HTTPServer.Builder()
.withDaemonThreads(true)
.withPort(Const.config.metrics.port)
.withHostname(Const.config.metrics.bindAddress)
.withPort(config.port)
.withHostname(config.bindAddress)
val httpServer = builder
.build()
@ -45,7 +56,7 @@ fun startMetricsServer() {
log.info { "运行指标服务器已启动. (Port: ${httpServer.port})" }
}
internal class Launcher : AutoCloseable {
internal class Launcher(private val config: AppConfig = Const.config) : AutoCloseable {
companion object {
@JvmStatic
@ -54,16 +65,46 @@ internal class Launcher : AutoCloseable {
private val botApi = TelegramBotsApi(DefaultBotSession::class.java)
private val botSessionMap = mutableMapOf<ScalaBot, BotSession>()
private val mavenLocalRepository =
if (Const.config.mavenLocalRepository != null && Const.config.mavenLocalRepository.isNotEmpty()) {
val repoPath = AppPaths.DATA_ROOT.file.toPath()
.resolve(Const.config.mavenLocalRepository)
.toRealPath()
.toFile()
LocalRepository(repoPath)
} else {
LocalRepository("${System.getProperty("user.home")}/.m2/repository")
private val mavenLocalRepository = getMavenLocalRepository()
private fun getMavenLocalRepository(): LocalRepository {
val localPath =
if (config.mavenLocalRepository != null && config.mavenLocalRepository.isNotEmpty()) {
val repoPath = AppPaths.DATA_ROOT.file.toPath()
.resolve(config.mavenLocalRepository)
.apply {
if (!exists()) {
if (!parent.isWritable() || !parent.isReadable()) {
throw IOException("Unable to read and write the directory where Maven repository is located.")
}
if (System.getProperty("os.name").lowercase().startsWith("windows")) {
createDirectories()
} else {
createDirectories(
PosixFilePermissions.asFileAttribute(
setOf(
PosixFilePermission.OWNER_READ,
PosixFilePermission.OWNER_WRITE,
PosixFilePermission.GROUP_READ,
PosixFilePermission.GROUP_WRITE,
PosixFilePermission.OTHERS_READ,
)
)
)
}
}
}
.toRealPath()
.toFile()
repoPath
} else {
File("${System.getProperty("user.home")}/.m2/repository")
}
if (!localPath.exists()) {
localPath.mkdirs()
}
return LocalRepository(localPath)
}
@Synchronized
fun launch(): Boolean {
@ -71,6 +112,9 @@ internal class Launcher : AutoCloseable {
if (botConfigs.isEmpty()) {
log.warn { "尚未配置任何机器人, 请先配置机器人后再启动本程序." }
return false
} else if (botConfigs.none { it.enabled }) {
log.warn { "配置文件中没有已启用的机器人, 请至少启用一个机器人." }
return false
}
for (botConfig in botConfigs) {
try {
@ -92,15 +136,15 @@ internal class Launcher : AutoCloseable {
val proxyConfig =
if (botConfig.proxy != null && botConfig.proxy.type != DefaultBotOptions.ProxyType.NO_PROXY) {
botConfig.proxy
} else if (Const.config.proxy.type != DefaultBotOptions.ProxyType.NO_PROXY) {
Const.config.proxy
} else if (config.proxy.type != DefaultBotOptions.ProxyType.NO_PROXY) {
config.proxy
} else {
null
}
if (proxyConfig != null) {
proxyType = proxyConfig.type
proxyHost = Const.config.proxy.host
proxyPort = Const.config.proxy.port
proxyHost = config.proxy.host
proxyPort = config.proxy.port
log.debug { "机器人 `${botConfig.account.name}` 已启用代理配置: $proxyConfig" }
}
@ -110,16 +154,21 @@ internal class Launcher : AutoCloseable {
}
val account = botConfig.account
val remoteRepositories = Const.config.mavenRepositories
.map(MavenRepositoryConfig::toRemoteRepository)
val remoteRepositories = config.mavenRepositories
.map { it.toRemoteRepository(config.proxy) }
.toMutableList().apply {
add(MavenRepositoryExtensionFinder.getMavenCentralRepository(proxy = Const.config.proxy.toAetherProxy()))
if (this.none {
it.url == MavenRepositoryExtensionFinder.MAVEN_CENTRAL_URL
|| it.url == MavenRepositoryExtensionFinder.MAVEN_CENTRAL_URL.trimEnd('/')
}) {
add(MavenRepositoryExtensionFinder.getMavenCentralRepository(proxy = config.proxy.toAetherProxy()))
}
}.toList()
val extensionPackageFinders = setOf(
MavenRepositoryExtensionFinder(
localRepository = mavenLocalRepository,
remoteRepositories = remoteRepositories,
proxy = Const.config.proxy.toAetherProxy()
proxy = config.proxy.toAetherProxy()
)
)

View File

@ -2,7 +2,7 @@ package net.lamgc.scalabot
import com.google.common.io.Files
import mu.KotlinLogging
import net.lamgc.scalabot.util.toHaxString
import net.lamgc.scalabot.util.toHexString
import org.mapdb.DB
import org.mapdb.DBException
import org.mapdb.DBMaker
@ -186,5 +186,5 @@ private object BotAccountIdDbAdapter : FileDbAdapter("BotAccountId", { botAccoun
private object BotTokenDbAdapter : FileDbAdapter("BotToken_v0.1.0", { botAccount ->
val digest: MessageDigest = MessageDigest.getInstance("SHA-256")
val digestBytes = digest.digest(botAccount.token.toByteArray(StandardCharsets.UTF_8))
File(AppPaths.DATA_DB.file, "${digestBytes.toHaxString()}.db")
File(AppPaths.DATA_DB.file, "${digestBytes.toHexString()}.db")
})

View File

@ -132,7 +132,7 @@ internal class ExtensionLoader(
result[finder] = artifacts
}
} catch (e: Exception) {
log.error { "搜索器 ${finder::class.java.name} 在搜索扩展 `$extensionArtifact` 时发生错误:" }
log.error(e) { "搜索器 ${finder::class.java.name} 在搜索扩展 `$extensionArtifact` 时发生错误." }
}
}
return result

View File

@ -257,9 +257,21 @@ internal class MavenRepositoryExtensionFinder(
}
override fun findByArtifact(extensionArtifact: Artifact, extensionsPath: File): Set<FoundExtensionPackage> {
log.debug {
StringBuilder().apply {
append("构件 $extensionArtifact 将在以下仓库拉取: \n")
remoteRepositories.forEach {
append("\t- ${it}\n")
}
}
}
val extensionArtifactResult = repositorySystem.resolveArtifact(
repoSystemSession,
ArtifactRequest(extensionArtifact, remoteRepositories, null)
ArtifactRequest(
extensionArtifact,
repositorySystem.newResolutionRepositories(repoSystemSession, remoteRepositories),
null
)
)
val extResolvedArtifact = extensionArtifactResult.artifact
if (!extensionArtifactResult.isResolved) {

View File

@ -103,6 +103,12 @@ internal class ScalaBot(
}
BotCommand(it.name(), abilityInfo)
}
if (botCommands.isEmpty()) {
log.info { "Bot 没有任何命令, 命令列表更新已跳过." }
return true
}
val setMyCommands = SetMyCommands()
setMyCommands.commands = botCommands
return execute(DeleteMyCommands()) && execute(setMyCommands)
@ -114,7 +120,6 @@ internal class ScalaBot(
}
override fun onClosing() {
super.onClosing()
onlineBotGauge.dec()
}
@ -151,7 +156,7 @@ internal class ScalaBot(
private val updateProcessTime = Summary.build()
.name("update_process_duration_seconds")
.help(
"Time to process update. (This indicator includes the pre-processing of update by TelegrammBots, " +
"Time to process update. (This indicator includes the pre-processing of update by TelegramBots, " +
"so it may be different from the actual execution time of ability. " +
"It is not recommended to use it as the accurate execution time of ability)"
)

View File

@ -142,12 +142,15 @@ internal object MavenRepositoryConfigSerializer
return when (json) {
is JsonObject -> {
MavenRepositoryConfig(
id = json.get("id")?.asString,
url = URL(checkJsonKey(json, "url")),
proxy = if (json.has("proxy") && json.get("proxy").isJsonObject)
context.deserialize<Proxy>(
json.getAsJsonObject("proxy"), Proxy::class.java
) else null,
layout = json.get("layout").asString ?: "default",
enableReleases = json.get("enableReleases")?.asBoolean ?: true,
enableSnapshots = json.get("enableSnapshots")?.asBoolean ?: true,
authentication = if (json.has("authentication") && json.get("authentication").isJsonObject)
context.deserialize<Authentication>(
json.getAsJsonObject("authentication"), Authentication::class.java
@ -155,7 +158,7 @@ internal object MavenRepositoryConfigSerializer
)
}
is JsonPrimitive -> {
MavenRepositoryConfig(URL(json.asString))
MavenRepositoryConfig(url = URL(json.asString))
}
else -> {
throw JsonParseException("Unsupported Maven warehouse configuration type.")

View File

@ -7,9 +7,8 @@ import org.eclipse.aether.artifact.Artifact
import java.io.File
import java.io.FileFilter
import java.io.FilenameFilter
import java.net.URL
internal fun ByteArray.toHaxString(): String = ByteUtils.bytesToHexString(this)
internal fun ByteArray.toHexString(): String = joinToString("") { it.toString(16) }
internal fun Artifact.equalsArtifact(that: Artifact): Boolean =
this.groupId.equals(that.groupId) &&
@ -56,9 +55,14 @@ internal fun File.deepListFiles(
* @return 获取 Finder 的优先级.
* @throws NoSuchFieldException 如果 Finder 没有添加 [FinderRules] 注解时抛出该异常.
*/
internal fun ExtensionPackageFinder.getPriority() =
this::class.java.getDeclaredAnnotation(FinderRules::class.java)?.priority
internal fun ExtensionPackageFinder.getPriority(): Int {
val value = this::class.java.getDeclaredAnnotation(FinderRules::class.java)?.priority
?: throw NoSuchFieldException("Finder did not add `FinderRules` annotation")
if (value < 0) {
throw IllegalArgumentException("Priority cannot be lower than 0. (Class: ${this::class.java})")
}
return value
}
/**
* 为 [AutoCloseable] 对象注册 Jvm Shutdown 钩子.
@ -76,30 +80,19 @@ private object UtilsInternal {
val autoCloseableSet = mutableSetOf<AutoCloseable>()
init {
Runtime.getRuntime().addShutdownHook(Thread({
log.debug { "Closing registered hook resources..." }
autoCloseableSet.forEach {
try {
it.close()
} catch (e: Exception) {
log.error(e) { "An exception occurred while closing the resource. (Resource: `$it`)" }
}
Runtime.getRuntime().addShutdownHook(Thread(this::doCloseResources, "Shutdown-AutoCloseable"))
}
fun doCloseResources() {
log.debug { "Closing registered hook resources..." }
autoCloseableSet.removeIf {
try {
it.close()
} catch (e: Exception) {
log.error(e) { "An exception occurred while closing the resource. (Resource: `$it`)" }
}
log.debug { "All registered hook resources have been closed." }
}, "Shutdown-AutoCloseable"))
}
}
fun URL.resolveToFile(canonical: Boolean = true): File {
if ("file" != protocol) {
throw ClassCastException("Only the URL of the `file` protocol can be converted into a File object.")
}
val urlString = toString().substringAfter(':')
val file = File(urlString)
return if (canonical) {
file.canonicalFile
} else {
file
true
}
log.debug { "All registered hook resources have been closed." }
}
}

View File

@ -1,12 +1,19 @@
package net.lamgc.scalabot
import com.google.gson.Gson
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import org.junit.jupiter.api.io.TempDir
import java.io.File
import java.util.*
import kotlin.math.abs
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
class BotAccountTest {
internal class BotAccountTest {
@Test
fun deserializerTest() {
@ -29,3 +36,87 @@ class BotAccountTest {
}
internal class AppPathsTest {
@Test
fun `Data root path priority`() {
System.setProperty("bot.path.data", "A")
assertEquals("A", AppPaths.DATA_ROOT.file.path, "`DATA_ROOT`没有优先返回 Property 的值.")
System.getProperties().remove("bot.path.data")
if (System.getenv("BOT_DATA_PATH") != null) {
assertEquals(
System.getenv("BOT_DATA_PATH"), AppPaths.DATA_ROOT.file.path,
"`DATA_ROOT`没有返回 env 的值."
)
} else {
assertEquals(
System.getProperty("user.dir"), AppPaths.DATA_ROOT.file.path,
"`DATA_ROOT`没有返回 `user.dir` 的值."
)
val userDir = System.getProperty("user.dir")
System.getProperties().remove("user.dir")
assertEquals(".", AppPaths.DATA_ROOT.file.path, "`DATA_ROOT`没有返回 `.`(当前目录).")
System.setProperty("user.dir", userDir)
assertNotNull(System.getProperty("user.dir"), "环境还原失败!")
}
}
@Test
fun `default initializer`(@TempDir testDir: File) {
val defaultInitializerMethod = Class.forName("net.lamgc.scalabot.AppConfigsKt")
.getDeclaredMethod("defaultInitializer", AppPaths::class.java)
.apply { isAccessible = true }
val dirPath = "${testDir.canonicalPath}/directory/"
val dirFile = File(dirPath)
mockk<AppPaths> {
every { file }.returns(File(dirPath))
every { path }.returns(dirPath)
every { initial() }.answers {
defaultInitializerMethod.invoke(null, this@mockk)
}
}.initial()
assertTrue(dirFile.exists() && dirFile.isDirectory, "默认初始器未正常初始化【文件夹】.")
File(testDir, "test.txt").apply {
mockk<AppPaths> {
every { file }.returns(this@apply)
every { path }.returns(this@apply.canonicalPath)
every { initial() }.answers {
defaultInitializerMethod.invoke(null, this@mockk)
}
}.initial()
assertTrue(this@apply.exists() && this@apply.isFile, "默认初始器未正常初始化【文件】.")
}
val alreadyExistsFile = File("${testDir.canonicalPath}/alreadyExists.txt").apply {
if (!exists()) {
createNewFile()
}
}
assertTrue(alreadyExistsFile.exists(), "文件状态与预期不符.")
mockk<File> {
every { exists() }.returns(true)
every { canonicalPath }.answers { alreadyExistsFile.canonicalPath }
every { createNewFile() }.answers { alreadyExistsFile.createNewFile() }
every { mkdirs() }.answers { alreadyExistsFile.mkdirs() }
every { mkdir() }.answers { alreadyExistsFile.mkdir() }
}.apply {
mockk<AppPaths> {
every { file }.returns(this@apply)
every { path }.returns(this@apply.canonicalPath)
every { initial() }.answers {
defaultInitializerMethod.invoke(null, this@mockk)
}
}.initial()
verify(exactly = 0) { createNewFile() }
verify(exactly = 0) { mkdir() }
verify(exactly = 0) { mkdirs() }
}
defaultInitializerMethod.isAccessible = false
}
}

View File

@ -1,9 +1,21 @@
package net.lamgc.scalabot.util
import io.mockk.every
import io.mockk.justRun
import io.mockk.mockk
import io.mockk.verify
import net.lamgc.scalabot.ExtensionPackageFinder
import net.lamgc.scalabot.FinderPriority
import net.lamgc.scalabot.FinderRules
import net.lamgc.scalabot.FoundExtensionPackage
import org.eclipse.aether.artifact.Artifact
import org.eclipse.aether.artifact.DefaultArtifact
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertDoesNotThrow
import org.junit.jupiter.api.assertThrows
import java.io.File
import java.lang.reflect.InvocationTargetException
import java.nio.charset.StandardCharsets
import kotlin.test.*
internal class UtilsKtTest {
@ -16,4 +28,92 @@ internal class UtilsKtTest {
.equalsArtifact(DefaultArtifact("com.example:demo-2:1.0.0-SNAPSHOT"))
)
}
@Test
fun `bytes to hex`() {
assertEquals("48656c6c6f20576f726c64", "Hello World".toByteArray(StandardCharsets.UTF_8).toHexString())
}
@Test
fun `ExtensionPackageFinder - getPriority`() {
open class BaseTestFinder : ExtensionPackageFinder {
override fun findByArtifact(extensionArtifact: Artifact, extensionsPath: File): Set<FoundExtensionPackage> {
throw IllegalStateException("Calling this class is not allowed.")
}
}
@FinderRules(FinderPriority.ALTERNATE)
class StandardTestFinder : BaseTestFinder()
assertEquals(
FinderPriority.ALTERNATE, StandardTestFinder().getPriority(),
"获取到的优先值与预期不符"
)
@FinderRules(-1)
class OutOfRangePriorityFinder : BaseTestFinder()
assertThrows<IllegalArgumentException>("getPriority 方法没有对超出范围的优先值抛出异常.") {
OutOfRangePriorityFinder().getPriority()
}
class NoAnnotationFinder : BaseTestFinder()
assertThrows<NoSuchFieldException> {
NoAnnotationFinder().getPriority()
}
}
@Test
fun `AutoCloseable shutdown hook`() {
val utilsInternalClass = Class.forName("net.lamgc.scalabot.util.UtilsInternal")
val utilsInternalObject = utilsInternalClass.getDeclaredField("INSTANCE").get(null)
?: fail("无法获取 UtilsInternal 对象.")
val doCloseResourcesMethod = utilsInternalClass.getDeclaredMethod("doCloseResources")
.apply {
isAccessible = true
}
// 正常的运行过程.
val mockResource = mockk<AutoCloseable> {
justRun { close() }
}.registerShutdownHook()
doCloseResourcesMethod.invoke(utilsInternalObject)
verify { mockResource.close() }
// 异常捕获检查.
val exceptionMockResource = mockk<AutoCloseable> {
every { close() } throws RuntimeException("Expected exception.")
}.registerShutdownHook()
assertDoesNotThrow("在关闭资源时出现未捕获异常.") {
doCloseResourcesMethod.invoke(utilsInternalObject)
}
verify { exceptionMockResource.close() }
// 错误抛出检查.
val errorMockResource = mockk<AutoCloseable> {
every { close() } throws Error("Expected error.")
}.registerShutdownHook()
assertThrows<Error>("关闭资源时捕获了不该捕获的 Error.") {
try {
doCloseResourcesMethod.invoke(utilsInternalObject)
} catch (e: InvocationTargetException) {
throw e.targetException
}
}
verify { errorMockResource.close() }
@Suppress("UNCHECKED_CAST")
val resourceSet = utilsInternalClass.getDeclaredMethod("getAutoCloseableSet").invoke(utilsInternalObject)
as MutableSet<AutoCloseable>
resourceSet.clear()
val closeRef = mockk<AutoCloseable> {
justRun { close() }
}
resourceSet.add(closeRef)
assertTrue(resourceSet.contains(closeRef), "测试用资源虚引用添加失败.")
doCloseResourcesMethod.invoke(utilsInternalObject)
assertFalse(resourceSet.contains(closeRef), "资源虚引用未从列表中删除.")
resourceSet.clear()
}
}

View File

@ -4,6 +4,7 @@ plugins {
kotlin("jvm") version "1.6.10"
java
`maven-publish`
signing
}
dependencies {
@ -38,24 +39,21 @@ tasks.withType<KotlinCompile> {
publishing {
repositories {
val repoRootKey = "maven.repo.local.root"
val snapshot = project.version.toString().endsWith("-SNAPSHOT")
val repoRoot = System.getProperty(repoRootKey)?.trim()
if (repoRoot == null || repoRoot.isEmpty()) {
logger.warn(
"\"$repoRootKey\" configuration item is not specified, " +
"please add start parameter \"-D$repoRootKey {localPublishRepo}\"" +
" (if you are not currently executing the publish task, " +
"you can ignore this information)"
)
return@repositories
}
val repoUri = if (snapshot) {
uri("$repoRoot/snapshots")
if (project.version.toString().endsWith("-SNAPSHOT")) {
maven("https://repo.lamgc.moe/repository/maven-snapshots/") {
credentials {
username = project.properties["repo.credentials.private.username"].toString()
password = project.properties["repo.credentials.private.password"].toString()
}
}
} else {
uri("$repoRoot/releases")
maven("https://repo.lamgc.moe/repository/maven-releases/") {
credentials {
username = project.properties["repo.credentials.private.username"].toString()
password = project.properties["repo.credentials.private.password"].toString()
}
}
}
maven(repoUri)
}
publications {
@ -97,3 +95,8 @@ publishing {
}
}
signing {
useGpgCmd()
sign(publishing.publications["maven"])
}