diff --git a/scalabot-app/src/main/kotlin/util/Utils.kt b/scalabot-app/src/main/kotlin/util/Utils.kt index 4d9d566..550aae3 100644 --- a/scalabot-app/src/main/kotlin/util/Utils.kt +++ b/scalabot-app/src/main/kotlin/util/Utils.kt @@ -81,17 +81,20 @@ private object UtilsInternal { val autoCloseableSet = mutableSetOf() 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")) + true + } + log.debug { "All registered hook resources have been closed." } } } diff --git a/scalabot-app/src/test/kotlin/util/UtilsKtTest.kt b/scalabot-app/src/test/kotlin/util/UtilsKtTest.kt index ac88b55..7881d33 100644 --- a/scalabot-app/src/test/kotlin/util/UtilsKtTest.kt +++ b/scalabot-app/src/test/kotlin/util/UtilsKtTest.kt @@ -1,18 +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.assertDoesNotThrow import org.junit.jupiter.api.assertThrows import java.io.File +import java.lang.reflect.InvocationTargetException import java.nio.charset.StandardCharsets -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertTrue +import kotlin.test.* internal class UtilsKtTest { @@ -58,4 +61,59 @@ internal class UtilsKtTest { } } + + @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 { + justRun { close() } + }.registerShutdownHook() + doCloseResourcesMethod.invoke(utilsInternalObject) + verify { mockResource.close() } + + // 异常捕获检查. + val exceptionMockResource = mockk { + every { close() } throws RuntimeException("Expected exception.") + }.registerShutdownHook() + assertDoesNotThrow("在关闭资源时出现未捕获异常.") { + doCloseResourcesMethod.invoke(utilsInternalObject) + } + verify { exceptionMockResource.close() } + + // 错误抛出检查. + val errorMockResource = mockk { + every { close() } throws Error("Expected error.") + }.registerShutdownHook() + assertThrows("关闭资源时捕获了不该捕获的 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 + resourceSet.clear() + + val closeRef = mockk { + justRun { close() } + } + resourceSet.add(closeRef) + assertTrue(resourceSet.contains(closeRef), "测试用资源虚引用添加失败.") + doCloseResourcesMethod.invoke(utilsInternalObject) + assertFalse(resourceSet.contains(closeRef), "资源虚引用未从列表中删除.") + + resourceSet.clear() + } } \ No newline at end of file