diff --git a/mirai-core-api/src/commonMain/kotlin/event/events/bot.kt b/mirai-core-api/src/commonMain/kotlin/event/events/bot.kt index 0b4f2b08e..67846cbe4 100644 --- a/mirai-core-api/src/commonMain/kotlin/event/events/bot.kt +++ b/mirai-core-api/src/commonMain/kotlin/event/events/bot.kt @@ -88,6 +88,7 @@ public sealed class BotOfflineEvent : BotEvent, AbstractEvent() { * 因 returnCode = -10008 等原因掉线 */ @MiraiInternalApi("This is very experimental and might be changed") + @Deprecated("Deprecated with no replacement", level = DeprecationLevel.ERROR) public data class PacketFactoryErrorCode @MiraiInternalApi public constructor( val returnCode: Int, public override val bot: Bot, diff --git a/mirai-core-utils/src/commonMain/kotlin/ExceptionCollector.kt b/mirai-core-utils/src/commonMain/kotlin/ExceptionCollector.kt index 69a2e15a6..02ca56ca6 100644 --- a/mirai-core-utils/src/commonMain/kotlin/ExceptionCollector.kt +++ b/mirai-core-utils/src/commonMain/kotlin/ExceptionCollector.kt @@ -12,7 +12,10 @@ package net.mamoe.mirai.utils import kotlin.contracts.InvocationKind import kotlin.contracts.contract -public class ExceptionCollector { +public class ExceptionCollector : Sequence { + + // TODO: 2021/4/20 drop last + public constructor() public constructor(initial: Throwable?) { collect(initial) @@ -32,6 +35,12 @@ public class ExceptionCollector { if (e == null) return val last = last if (last != null) { + last.itr().forEach { suppressed -> + if (suppressed.stackTrace.contentEquals(e.stackTrace)) { + // filter out useless duplicates. + return + } + } e.addSuppressed(last) } this.last = e @@ -57,6 +66,15 @@ public class ExceptionCollector { @DslMarker private annotation class TerminalOperation + + private fun Throwable.itr(): Iterator { + return (sequenceOf(this) + this.suppressed.asSequence().flatMap { it.itr().asSequence() }).iterator() + } + + override fun iterator(): Iterator { + val last = getLast() ?: return emptyList().iterator() + return last.itr() + } } /** diff --git a/mirai-core/src/androidTest/kotlin/test/initPlatform.android.kt b/mirai-core/src/androidTest/kotlin/test/initPlatform.android.kt index 94ac23a97..8c292c56b 100644 --- a/mirai-core/src/androidTest/kotlin/test/initPlatform.android.kt +++ b/mirai-core/src/androidTest/kotlin/test/initPlatform.android.kt @@ -10,14 +10,36 @@ package net.mamoe.mirai.internal.test import net.mamoe.mirai.utils.MiraiLogger +import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.junit.jupiter.api.Test +import java.security.Security +import kotlin.test.assertTrue internal actual fun initPlatform() { init } -private val init by lazy { +private val init: Unit by lazy { MiraiLogger.setDefaultLoggerCreator { @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") net.mamoe.mirai.internal.utils.StdoutLogger(it) } + + if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) != null) { + Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME) + } + Security.addProvider(BouncyCastleProvider()) + + Unit +} + +internal actual class PlatformInitializationTest : AbstractTest() { + + @Test + actual fun test() { + assertTrue { + @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") + MiraiLogger.create("1") is net.mamoe.mirai.internal.utils.StdoutLogger + } + } } \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/QQAndroidBot.kt b/mirai-core/src/commonMain/kotlin/QQAndroidBot.kt index c6f5bd88a..005fd12b1 100644 --- a/mirai-core/src/commonMain/kotlin/QQAndroidBot.kt +++ b/mirai-core/src/commonMain/kotlin/QQAndroidBot.kt @@ -27,7 +27,9 @@ import net.mamoe.mirai.internal.network.components.* import net.mamoe.mirai.internal.network.context.SsoProcessorContext import net.mamoe.mirai.internal.network.context.SsoProcessorContextImpl import net.mamoe.mirai.internal.network.handler.NetworkHandler +import net.mamoe.mirai.internal.network.handler.NetworkHandler.State import net.mamoe.mirai.internal.network.handler.NetworkHandlerContextImpl +import net.mamoe.mirai.internal.network.handler.NetworkHandlerSupport import net.mamoe.mirai.internal.network.handler.selector.FactoryKeepAliveNetworkHandlerSelector import net.mamoe.mirai.internal.network.handler.selector.SelectorNetworkHandler import net.mamoe.mirai.internal.network.handler.state.* @@ -35,7 +37,6 @@ import net.mamoe.mirai.internal.network.impl.netty.NettyNetworkHandlerFactory import net.mamoe.mirai.internal.network.impl.netty.asCoroutineExceptionHandler import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacket import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacketWithRespType -import net.mamoe.mirai.internal.network.protocol.packet.login.StatSvc import net.mamoe.mirai.utils.BotConfiguration import net.mamoe.mirai.utils.MiraiLogger import net.mamoe.mirai.utils.systemProp @@ -61,7 +62,7 @@ internal class BotDebugConfiguration( ) @Suppress("INVISIBLE_MEMBER", "BooleanLiteralArgument", "OverridingDeprecatedMember") -internal class QQAndroidBot constructor( +internal open class QQAndroidBot constructor( internal val account: BotAccount, configuration: BotConfiguration, private val debugConfiguration: BotDebugConfiguration = BotDebugConfiguration(), @@ -77,25 +78,42 @@ internal class QQAndroidBot constructor( // TODO: 2021/4/14 bdhSyncer.loadFromCache() when login // IDE error, don't move into lazy - private fun ComponentStorage.stateObserverChain(): StateObserver { + fun ComponentStorage.stateObserverChain(): StateObserver { val components = this return StateObserver.chainOfNotNull( - components[BotInitProcessor].asObserver().safe(networkLogger), - StateChangedObserver(NetworkHandler.State.OK) { new -> - new.launch(logger.asCoroutineExceptionHandler()) { + components[BotInitProcessor].asObserver(), + StateChangedObserver(State.OK) { new -> + bot.launch(logger.asCoroutineExceptionHandler()) { BotOnlineEvent(bot).broadcast() if (bot.firstLoginSucceed) { // TODO: 2021/4/21 actually no use BotReloginEvent(bot, new.getCause()).broadcast() } } }, - StateChangedObserver(NetworkHandler.State.CLOSED) { new -> - new.launch(logger.asCoroutineExceptionHandler()) { - BotOfflineEvent.Dropped(bot, new.getCause()).broadcast() + object : StateObserver { + override fun stateChanged( + networkHandler: NetworkHandlerSupport, + previous: NetworkHandlerSupport.BaseStateImpl, + new: NetworkHandlerSupport.BaseStateImpl + ) { + val p = previous.correspondingState + val n = new.correspondingState + when { + p == State.OK && n == State.CONNECTING -> { + bot.launch(logger.asCoroutineExceptionHandler()) { + BotOfflineEvent.Dropped(bot, new.getCause()).broadcast() + } + } + p == State.OK && n == State.CLOSED -> { + bot.launch(logger.asCoroutineExceptionHandler()) { + BotOfflineEvent.Active(bot, new.getCause()).broadcast() + } + } + } } }, debugConfiguration.stateObserver - ) + ).safe(logger) } @@ -135,7 +153,7 @@ internal class QQAndroidBot constructor( val client get() = components[SsoProcessor].client override suspend fun sendLogout() { - network.sendWithoutExpect(StatSvc.Register.offline(client)) + components[SsoProcessor].logout(network) } override fun createNetworkHandler(): NetworkHandler { diff --git a/mirai-core/src/commonMain/kotlin/network/component/ConcurrentComponentStorage.kt b/mirai-core/src/commonMain/kotlin/network/component/ConcurrentComponentStorage.kt index ff78a0186..5ebe63919 100644 --- a/mirai-core/src/commonMain/kotlin/network/component/ConcurrentComponentStorage.kt +++ b/mirai-core/src/commonMain/kotlin/network/component/ConcurrentComponentStorage.kt @@ -11,6 +11,7 @@ package net.mamoe.mirai.internal.network.component import net.mamoe.mirai.utils.systemProp import java.util.concurrent.ConcurrentHashMap +import kotlin.LazyThreadSafetyMode.NONE /** * A thread-safe implementation of [MutableComponentStorage] @@ -55,4 +56,4 @@ internal class ConcurrentComponentStorage( } } -private val SHOW_ALL_COMPONENTS = systemProp("mirai.debug.network.show.all.components", false) \ No newline at end of file +private val SHOW_ALL_COMPONENTS: Boolean by lazy(NONE) { systemProp("mirai.debug.network.show.all.components", false) } \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/network/components/BotInitProcessor.kt b/mirai-core/src/commonMain/kotlin/network/components/BotInitProcessor.kt index 60e818f3f..afad57e78 100644 --- a/mirai-core/src/commonMain/kotlin/network/components/BotInitProcessor.kt +++ b/mirai-core/src/commonMain/kotlin/network/components/BotInitProcessor.kt @@ -34,7 +34,7 @@ import net.mamoe.mirai.utils.info * Facade of [ContactUpdater], [OtherClientUpdater], [ConfigPushSyncer]. * Handles initialization jobs after successful logon. * - * Attached to handler state [NetworkHandler.State.LOADING] [as state observer][asObserver]. + * Attached to handler state [NetworkHandler.State.LOADING] [as state observer][asObserver] in [QQAndroidBot.stateObserverChain]. */ internal interface BotInitProcessor { suspend fun init() diff --git a/mirai-core/src/commonMain/kotlin/network/components/SsoProcessor.kt b/mirai-core/src/commonMain/kotlin/network/components/SsoProcessor.kt index b16400c00..4fa1bb483 100644 --- a/mirai-core/src/commonMain/kotlin/network/components/SsoProcessor.kt +++ b/mirai-core/src/commonMain/kotlin/network/components/SsoProcessor.kt @@ -24,6 +24,7 @@ import net.mamoe.mirai.internal.network.handler.state.StateChangedObserver import net.mamoe.mirai.internal.network.handler.state.StateObserver import net.mamoe.mirai.internal.network.impl.netty.NettyNetworkHandler import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacketWithRespType +import net.mamoe.mirai.internal.network.protocol.packet.login.StatSvc import net.mamoe.mirai.internal.network.protocol.packet.login.WtLogin.Login.LoginPacketResponse import net.mamoe.mirai.internal.network.protocol.packet.login.WtLogin.Login.LoginPacketResponse.Captcha import net.mamoe.mirai.internal.network.protocol.packet.login.wtlogin.WtLogin10 @@ -57,6 +58,8 @@ internal interface SsoProcessor { @Throws(LoginFailedException::class) suspend fun login(handler: NetworkHandler) + suspend fun logout(handler: NetworkHandler) + companion object : ComponentKey } @@ -110,6 +113,10 @@ internal class SsoProcessorImpl( ssoContext.accountSecretsManager.saveSecrets(ssoContext.account, AccountSecretsImpl(client)) } + override suspend fun logout(handler: NetworkHandler) { + handler.sendWithoutExpect(StatSvc.Register.offline(client)) + } + private fun createClient(bot: QQAndroidBot): QQAndroidClient { val device = ssoContext.device return QQAndroidClient( diff --git a/mirai-core/src/commonMain/kotlin/network/handler/NetworkHandler.kt b/mirai-core/src/commonMain/kotlin/network/handler/NetworkHandler.kt index d04a69368..f80b209ae 100644 --- a/mirai-core/src/commonMain/kotlin/network/handler/NetworkHandler.kt +++ b/mirai-core/src/commonMain/kotlin/network/handler/NetworkHandler.kt @@ -10,22 +10,25 @@ package net.mamoe.mirai.internal.network.handler import kotlinx.coroutines.selects.SelectClause1 +import net.mamoe.mirai.Bot import net.mamoe.mirai.internal.network.Packet -import net.mamoe.mirai.internal.network.handler.NetworkHandler.State +import net.mamoe.mirai.internal.network.components.BotInitProcessor +import net.mamoe.mirai.internal.network.components.SsoProcessor +import net.mamoe.mirai.internal.network.handler.selector.SelectorNetworkHandler +import net.mamoe.mirai.internal.network.handler.state.StateObserver import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacket import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacketWithRespType import net.mamoe.mirai.utils.MiraiLogger -import java.net.InetAddress -import java.net.InetSocketAddress -import java.net.SocketAddress -import java.util.concurrent.CancellationException /** * Basic interface available to application. Usually wrapped with [SelectorNetworkHandler]. * * Implementation is usually subclass of [NetworkHandlerSupport]. * + * Instances are often created by [NetworkHandlerFactory]. + * * @see NetworkHandlerSupport + * @see NetworkHandlerFactory */ internal interface NetworkHandler { val context: NetworkHandlerContext @@ -33,7 +36,7 @@ internal interface NetworkHandler { fun isOk() = state == State.OK /** - * State of this handler. + * Current state of this handler. This is volatile. */ val state: State @@ -42,6 +45,28 @@ internal interface NetworkHandler { */ val onStateChanged: SelectClause1 + /** + * State of this handler. + * + * ## States transition overview + * + * There are 5 [State]s, each of which encapsulates the state of the network connection. + * + * Initial state is [State.INITIALIZED], at which no packets can be send before [resumeConnection], which transmits state into [State.CONNECTING]. + * On [State.CONNECTING], [NetworkHandler] establishes a connection with the server while [SsoProcessor] takes responsibility in the single-sign-on process. + * + * Successful logon turns state to [State.LOADING], an **open state**, which does nothing by default. Jobs can be *attached* by [StateObserver]s. + * For example, attaching a [BotInitProcessor] to handle session-relevant jobs to the [Bot]. + * + * Failure during [State.CONNECTING] and [State.LOADING] switches state to [State.CLOSED], on which [NetworkHandler] is considered permanently dead. + * + * The state after finish of [State.LOADING] is [State.OK]. This state lasts for the majority of time. + * + * When connection is lost (e.g. due to Internet unavailability), it returns to [State.CONNECTING] and repeatedly attempts to reconnect. + * Immediately after successful recovery, [State.OK] will be set. + * + * @see state + */ enum class State { /** * Just created and no connection has been made. @@ -73,18 +98,18 @@ internal interface NetworkHandler { OK, /** - * Cannot resume anymore. Both [resumeConnection] and [sendAndExpect] throw a [CancellationException]. + * The terminal state. Cannot resume anymore. Both [resumeConnection] and [sendAndExpect] throw a [IllegalStateException]. * * When a handler reached [CLOSED] state, it is finalized and cannot be restored to any other states. * * At this state [resumeConnection] throws the exception caught from underlying socket implementation (i.e netty). - * [sendAndExpect] throws [IllegalStateException] + * [sendAndExpect] throws [IllegalStateException]. */ CLOSED, } /** - * Attempts to resume the connection. + * Suspends the coroutine until [sendAndExpect] can be executed without suspension. * * May throw exception that had caused current state to fail. * @see State @@ -95,17 +120,23 @@ internal interface NetworkHandler { /** * Sends [packet] and expects to receive a response from the server. + * + * Coroutine suspension may happen if connection if not yet available however, [IllegalStateException] is thrown if [NetworkHandler] is already in [State.CLOSED] + * * @param attempts ranges `1..INFINITY` */ suspend fun sendAndExpect(packet: OutgoingPacket, timeout: Long = 5000, attempts: Int = 2): Packet? /** - * Sends [packet] and does not expect any response. (Response is still processed but not passed as a return value of this function.) + * Sends [packet] and does not expect any response. + * + * Response is still being processed but not passed as a return value of this function, so it does not suspends this function. + * However, coroutine is still suspended if connection if not yet available, and [IllegalStateException] is thrown if [NetworkHandler] is already in [State.CLOSED] */ suspend fun sendWithoutExpect(packet: OutgoingPacket) /** - * Closes this handler gracefully. + * Closes this handler gracefully (i.e. asynchronously). */ fun close(cause: Throwable?) @@ -142,18 +173,3 @@ internal interface NetworkHandler { internal val NetworkHandler.logger: MiraiLogger get() = context.logger -/** - * Factory for a specific [NetworkHandler] implementation. - */ -internal interface NetworkHandlerFactory { - fun create(context: NetworkHandlerContext, host: String, port: Int): H = - create(context, InetSocketAddress.createUnresolved(host, port)) - - fun create(context: NetworkHandlerContext, host: InetAddress, port: Int): H = - create(context, InetSocketAddress(host, port)) - - /** - * Create an instance of [H]. The returning [H] has [NetworkHandler.state] of [State.INITIALIZED] - */ - fun create(context: NetworkHandlerContext, address: SocketAddress): H -} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/network/handler/NetworkHandlerFactory.kt b/mirai-core/src/commonMain/kotlin/network/handler/NetworkHandlerFactory.kt new file mode 100644 index 000000000..3278f9d3a --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/network/handler/NetworkHandlerFactory.kt @@ -0,0 +1,31 @@ +/* + * 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 + */ + +package net.mamoe.mirai.internal.network.handler + +import net.mamoe.mirai.internal.network.handler.NetworkHandler.State +import java.net.InetAddress +import java.net.InetSocketAddress +import java.net.SocketAddress + +/** + * Factory for a specific [NetworkHandler] implementation. + */ +internal interface NetworkHandlerFactory { + fun create(context: NetworkHandlerContext, host: String, port: Int): H = + create(context, InetSocketAddress.createUnresolved(host, port)) + + fun create(context: NetworkHandlerContext, host: InetAddress, port: Int): H = + create(context, InetSocketAddress(host, port)) + + /** + * Create an instance of [H]. The returning [H] has [NetworkHandler.state] of [State.INITIALIZED] + */ + fun create(context: NetworkHandlerContext, address: SocketAddress): H +} diff --git a/mirai-core/src/commonMain/kotlin/network/impl/netty/NettyNetworkHandler.kt b/mirai-core/src/commonMain/kotlin/network/impl/netty/NettyNetworkHandler.kt index dbe11e117..9b90287f7 100644 --- a/mirai-core/src/commonMain/kotlin/network/impl/netty/NettyNetworkHandler.kt +++ b/mirai-core/src/commonMain/kotlin/network/impl/netty/NettyNetworkHandler.kt @@ -36,7 +36,7 @@ import java.net.SocketAddress import kotlin.coroutines.CoroutineContext import io.netty.channel.Channel as NettyChannel -internal class NettyNetworkHandler( +internal open class NettyNetworkHandler( context: NetworkHandlerContext, private val address: SocketAddress, ) : NetworkHandlerSupport(context) { @@ -86,7 +86,16 @@ internal class NettyNetworkHandler( } } - private suspend fun createConnection(decodePipeline: PacketDecodePipeline): NettyChannel { + protected open fun setupChannelPipeline(pipeline: ChannelPipeline, decodePipeline: PacketDecodePipeline) { + pipeline + .addLast(OutgoingPacketEncoder()) + .addLast(LengthFieldBasedFrameDecoder(Int.MAX_VALUE, 0, 4, -4, 4)) + .addLast(ByteBufToIncomingPacketDecoder()) + .addLast(RawIncomingPacketCollector(decodePipeline)) + } + + // can be overridden for tests + protected open suspend fun createConnection(decodePipeline: PacketDecodePipeline): NettyChannel { val contextResult = CompletableDeferred() val eventLoopGroup = NioEventLoopGroup() @@ -101,10 +110,8 @@ internal class NettyNetworkHandler( eventLoopGroup.shutdownGracefully() } }) - .addLast(OutgoingPacketEncoder()) - .addLast(LengthFieldBasedFrameDecoder(Int.MAX_VALUE, 0, 4, -4, 4)) - .addLast(ByteBufToIncomingPacketDecoder()) - .addLast(RawIncomingPacketCollector(decodePipeline)) + + setupChannelPipeline(ch.pipeline(), decodePipeline) } }) .connect(address) @@ -125,9 +132,9 @@ internal class NettyNetworkHandler( return contextResult.await() } - private val decodePipeline = PacketDecodePipeline(this@NettyNetworkHandler.coroutineContext) + protected val decodePipeline = PacketDecodePipeline(this@NettyNetworkHandler.coroutineContext) - private inner class PacketDecodePipeline(parentContext: CoroutineContext) : + protected inner class PacketDecodePipeline(parentContext: CoroutineContext) : CoroutineScope by parentContext.childScope() { private val channel: Channel = Channel(Channel.BUFFERED) private val packetCodec: PacketCodec by lazy { context[PacketCodec] } @@ -159,13 +166,13 @@ internal class NettyNetworkHandler( * * @see StateObserver */ - private abstract inner class NettyState( + protected abstract inner class NettyState( correspondingState: State ) : BaseStateImpl(correspondingState) { abstract suspend fun sendPacketImpl(packet: OutgoingPacket) } - private inner class StateInitialized : NettyState(State.INITIALIZED) { + protected inner class StateInitialized : NettyState(State.INITIALIZED) { override suspend fun sendPacketImpl(packet: OutgoingPacket) { error("Cannot send packet when connection is not set. (resumeConnection not called.)") } @@ -181,16 +188,20 @@ internal class NettyNetworkHandler( /** * 1. Connect to server. * 2. Perform SSO login with [SsoProcessor] - * 3. If failure, set state to [StateClosed] - * 4. If success, set state to [StateOK] + * + * If failure, set state to [StateClosed] + * If success, set state to [StateOK] */ - private inner class StateConnecting( + protected inner class StateConnecting( /** * Collected (suppressed) exceptions that have led this state. * * Dropped when state becomes [StateOK]. */ private val collectiveExceptions: ExceptionCollector, + /** + * If `true`, [delay] 5 seconds before connecting. + */ wait: Boolean = false ) : NettyState(State.CONNECTING) { private val connection = async { @@ -238,7 +249,7 @@ internal class NettyNetworkHandler( * @see BotInitProcessor * @see StateObserver */ - private inner class StateLoading( + protected inner class StateLoading( private val connection: NettyChannel ) : NettyState(State.LOADING) { override suspend fun sendPacketImpl(packet: OutgoingPacket) { @@ -256,7 +267,7 @@ internal class NettyNetworkHandler( override fun toString(): String = "StateLoading" } - private inner class StateOK( + protected inner class StateOK( private val connection: NettyChannel ) : NettyState(State.OK) { init { @@ -317,7 +328,7 @@ internal class NettyNetworkHandler( override fun toString(): String = "StateOK" } - private inner class StateClosed( + protected inner class StateClosed( val exception: Throwable? ) : NettyState(State.CLOSED) { init { diff --git a/mirai-core/src/commonTest/kotlin/MockBot.kt b/mirai-core/src/commonTest/kotlin/MockBot.kt index cb5944b33..2b91c3d71 100644 --- a/mirai-core/src/commonTest/kotlin/MockBot.kt +++ b/mirai-core/src/commonTest/kotlin/MockBot.kt @@ -12,34 +12,46 @@ package net.mamoe.mirai.internal +import net.mamoe.mirai.internal.network.handler.NetworkHandler import net.mamoe.mirai.utils.BotConfiguration +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract internal val MockAccount = BotAccount(1, "pwd") internal val MockConfiguration = BotConfiguration { + randomDeviceInfo() } internal class MockBotBuilder( val conf: BotConfiguration = BotConfiguration(), - val debugConf: BotDebugConfiguration = BotDebugConfiguration() + val debugConf: BotDebugConfiguration = BotDebugConfiguration(), ) { + var nhProvider: (QQAndroidBot.(bot: QQAndroidBot) -> NetworkHandler)? = null + fun conf(action: BotConfiguration.() -> Unit): MockBotBuilder { + contract { callsInPlace(action, InvocationKind.EXACTLY_ONCE) } conf.apply(action) return this } fun debugConf(action: BotDebugConfiguration.() -> Unit): MockBotBuilder { + contract { callsInPlace(action, InvocationKind.EXACTLY_ONCE) } debugConf.apply(action) return this } + + fun networkHandlerProvider(provider: QQAndroidBot.(bot: QQAndroidBot) -> NetworkHandler): MockBotBuilder { + this.nhProvider = provider + return this + } } @Suppress("TestFunctionName") -internal fun MockBot(conf: MockBotBuilder.() -> Unit) = +internal fun MockBot(conf: MockBotBuilder.() -> Unit = {}) = MockBotBuilder(MockConfiguration.copy()).apply(conf).run { - QQAndroidBot(MockAccount, this.conf, debugConf) + object : QQAndroidBot(MockAccount, this.conf, debugConf) { + override fun createNetworkHandler(): NetworkHandler = + nhProvider?.invoke(this, this) ?: super.createNetworkHandler() + } } - -@Suppress("TestFunctionName") -internal fun MockBot() = - QQAndroidBot(MockAccount, MockConfiguration.copy()) \ No newline at end of file diff --git a/mirai-core/src/commonTest/kotlin/network/framework/AbstractNetworkEventTest.kt b/mirai-core/src/commonTest/kotlin/network/framework/AbstractRealNetworkHandlerTest.kt similarity index 76% rename from mirai-core/src/commonTest/kotlin/network/framework/AbstractNetworkEventTest.kt rename to mirai-core/src/commonTest/kotlin/network/framework/AbstractRealNetworkHandlerTest.kt index b75c03137..a3b833570 100644 --- a/mirai-core/src/commonTest/kotlin/network/framework/AbstractNetworkEventTest.kt +++ b/mirai-core/src/commonTest/kotlin/network/framework/AbstractRealNetworkHandlerTest.kt @@ -31,11 +31,20 @@ import java.net.InetSocketAddress /** * With real factory and components as in [QQAndroidBot.components]. */ -internal abstract class AbstractRealNetworkHandlerTest( - private val factory: NetworkHandlerFactory, -) : AbstractTest() { - val bot = MockBot() - val networkLogger = MiraiLogger.TopLevel +internal abstract class AbstractRealNetworkHandlerTest : AbstractTest() { + init { + System.setProperty("mirai.debug.network.state.observer.logging", "true") + System.setProperty("mirai.debug.network.show.all.components", "true") + } + + protected abstract val factory: NetworkHandlerFactory + + protected open val bot: QQAndroidBot by lazy { + MockBot { + networkHandlerProvider { createHandler() } + } + } + protected open val networkLogger = MiraiLogger.TopLevel protected open val defaultComponents = ConcurrentComponentStorage().apply { val components = this @@ -44,18 +53,27 @@ internal abstract class AbstractRealNetworkHandlerTest( set(SsoProcessor, object : SsoProcessor { override val client: QQAndroidClient get() = bot.client override val ssoSession: SsoSession get() = bot.client - override fun createObserverChain(): StateObserver = StateObserver.NOP + override fun createObserverChain(): StateObserver = get(StateObserver) override suspend fun login(handler: NetworkHandler) { networkLogger.debug { "SsoProcessor.login" } } + + override suspend fun logout(handler: NetworkHandler) { + networkLogger.debug { "SsoProcessor.logout" } + } }) set(HeartbeatProcessor, object : HeartbeatProcessor { override suspend fun doHeartbeatNow(networkHandler: NetworkHandler) { networkLogger.debug { "HeartbeatProcessor.doHeartbeatNow" } } }) - set(KeyRefreshProcessor, KeyRefreshProcessorImpl(networkLogger)) - set(ConfigPushProcessor, ConfigPushProcessorImpl(networkLogger)) + set(KeyRefreshProcessor, object : KeyRefreshProcessor { + override suspend fun keyRefreshLoop(handler: NetworkHandler) {} + override suspend fun refreshKeysNow(handler: NetworkHandler) {} + }) + set(ConfigPushProcessor, object : ConfigPushProcessor { + override suspend fun syncConfigPush(network: NetworkHandler) {} + }) set(BotInitProcessor, object : BotInitProcessor { override suspend fun init() { @@ -71,11 +89,10 @@ internal abstract class AbstractRealNetworkHandlerTest( set(OtherClientUpdater, OtherClientUpdaterImpl(bot, components, bot.logger)) set(ConfigPushSyncer, ConfigPushSyncerImpl()) - set(StateObserver, StateObserver.NOP) - + set(StateObserver, bot.run { stateObserverChain() }) } - protected open fun createHandler(additionalComponents: ComponentStorage? = null): NetworkHandler { + protected open fun createHandler(additionalComponents: ComponentStorage? = null): H { return factory.create( NetworkHandlerContextImpl( bot, diff --git a/mirai-core/src/commonTest/kotlin/network/framework/test/FrameworkEventTest.kt b/mirai-core/src/commonTest/kotlin/network/framework/test/FrameworkEventTest.kt new file mode 100644 index 000000000..bd7bd1f3a --- /dev/null +++ b/mirai-core/src/commonTest/kotlin/network/framework/test/FrameworkEventTest.kt @@ -0,0 +1,69 @@ +/* + * 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 + */ + +package net.mamoe.mirai.internal.network.framework.test + +import net.mamoe.mirai.event.AbstractEvent +import net.mamoe.mirai.event.broadcast +import net.mamoe.mirai.internal.test.AbstractTest +import net.mamoe.mirai.internal.test.assertEventBroadcasts +import net.mamoe.mirai.internal.test.runBlockingUnit +import kotlin.test.Test +import kotlin.test.assertFails + +internal class FrameworkEventTest : AbstractTest() { + + class TestEvent : AbstractEvent() + class TestEvent2 : AbstractEvent() + + @Test + fun `can observe event`() = runBlockingUnit { + assertEventBroadcasts { + TestEvent().broadcast() + } + } + + @Test + fun `observes expected event`() = runBlockingUnit { + assertEventBroadcasts(1) { + TestEvent().broadcast() + TestEvent2().broadcast() + } + } + + @Test + fun `can observe event multiple times`() = runBlockingUnit { + assertEventBroadcasts(2) { + TestEvent().broadcast() + TestEvent().broadcast() + } + } + + @Test + fun `can observe event only in block`() = runBlockingUnit { + TestEvent().broadcast() + assertEventBroadcasts(1) { + TestEvent().broadcast() + } + } + + @Test + fun `fails if times not match`() = runBlockingUnit { + assertFails { + assertEventBroadcasts(2) { + TestEvent().broadcast() + } + } + assertFails { + assertEventBroadcasts(0) { + TestEvent().broadcast() + } + } + } +} \ No newline at end of file diff --git a/mirai-core/src/commonTest/kotlin/network/handler/HandlerEventTest.kt b/mirai-core/src/commonTest/kotlin/network/handler/HandlerEventTest.kt deleted file mode 100644 index e02486c76..000000000 --- a/mirai-core/src/commonTest/kotlin/network/handler/HandlerEventTest.kt +++ /dev/null @@ -1,24 +0,0 @@ -/* - * 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 - */ - -package net.mamoe.mirai.internal.network.handler - -import net.mamoe.mirai.internal.network.framework.AbstractRealNetworkHandlerTest -import net.mamoe.mirai.internal.network.impl.netty.NettyNetworkHandler -import net.mamoe.mirai.internal.network.impl.netty.NettyNetworkHandlerFactory -import net.mamoe.mirai.internal.test.runBlockingUnit -import kotlin.test.Test - -internal class HandlerEventTest : AbstractRealNetworkHandlerTest(NettyNetworkHandlerFactory) { - - @Test - fun `BotOnlineEvent after successful logon`() = runBlockingUnit { - bot.login() - } -} \ No newline at end of file diff --git a/mirai-core/src/commonTest/kotlin/network/handler/StateObserverTest.kt b/mirai-core/src/commonTest/kotlin/network/handler/StateObserverTest.kt index 487d8d01c..1bfece691 100644 --- a/mirai-core/src/commonTest/kotlin/network/handler/StateObserverTest.kt +++ b/mirai-core/src/commonTest/kotlin/network/handler/StateObserverTest.kt @@ -12,6 +12,7 @@ package net.mamoe.mirai.internal.network.handler import net.mamoe.mirai.internal.network.framework.AbstractMockNetworkHandlerTest import net.mamoe.mirai.internal.network.handler.NetworkHandler.State.CONNECTING import net.mamoe.mirai.internal.network.handler.NetworkHandler.State.INITIALIZED +import net.mamoe.mirai.internal.network.handler.state.CombinedStateObserver.Companion.plus import net.mamoe.mirai.internal.network.handler.state.StateChangedObserver import net.mamoe.mirai.internal.network.handler.state.StateObserver import org.junit.jupiter.api.Test @@ -65,4 +66,36 @@ internal class StateObserverTest : AbstractMockNetworkHandlerTest() { assertEquals(INITIALIZED, called[0].first.correspondingState) assertEquals(CONNECTING, called[0].second.correspondingState) } + + @Test + fun `can combine`() { + val called = ArrayList>() + components[StateObserver] = object : StateChangedObserver(CONNECTING) { + override fun stateChanged0( + networkHandler: NetworkHandlerSupport, + previous: NetworkHandlerSupport.BaseStateImpl, + new: NetworkHandlerSupport.BaseStateImpl + ) { + called.add(previous to new) + } + } + object : StateChangedObserver(CONNECTING) { + override fun stateChanged0( + networkHandler: NetworkHandlerSupport, + previous: NetworkHandlerSupport.BaseStateImpl, + new: NetworkHandlerSupport.BaseStateImpl + ) { + called.add(previous to new) + } + } + val handler = createNetworkHandler() + assertEquals(0, called.size) + handler.setState(INITIALIZED) + assertEquals(0, called.size) + handler.setState(CONNECTING) + assertEquals(2, called.size) + assertEquals(INITIALIZED, called[0].first.correspondingState) + assertEquals(CONNECTING, called[0].second.correspondingState) + assertEquals(INITIALIZED, called[1].first.correspondingState) + assertEquals(CONNECTING, called[1].second.correspondingState) + } } \ No newline at end of file diff --git a/mirai-core/src/commonTest/kotlin/network/impl/netty/NettyHandlerEventTest.kt b/mirai-core/src/commonTest/kotlin/network/impl/netty/NettyHandlerEventTest.kt new file mode 100644 index 000000000..a6a71ec34 --- /dev/null +++ b/mirai-core/src/commonTest/kotlin/network/impl/netty/NettyHandlerEventTest.kt @@ -0,0 +1,110 @@ +/* + * 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 + */ + +package net.mamoe.mirai.internal.network.impl.netty + +import io.netty.channel.Channel +import io.netty.channel.embedded.EmbeddedChannel +import kotlinx.coroutines.delay +import net.mamoe.mirai.event.events.BotOfflineEvent +import net.mamoe.mirai.event.events.BotOnlineEvent +import net.mamoe.mirai.event.events.BotReloginEvent +import net.mamoe.mirai.internal.network.framework.AbstractRealNetworkHandlerTest +import net.mamoe.mirai.internal.network.handler.NetworkHandler.State +import net.mamoe.mirai.internal.network.handler.NetworkHandlerContext +import net.mamoe.mirai.internal.network.handler.NetworkHandlerFactory +import net.mamoe.mirai.internal.test.assertEventBroadcasts +import net.mamoe.mirai.internal.test.runBlockingUnit +import net.mamoe.mirai.utils.ExceptionCollector +import java.net.SocketAddress +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.time.seconds + + +internal open class TestNettyNH( + context: NetworkHandlerContext, + address: SocketAddress +) : NettyNetworkHandler(context, address) { + + fun setStateClosed(exception: Throwable? = null) { + setState { StateClosed(exception) } + } + + fun setStateConnecting(exception: Throwable? = null) { + setState { StateConnecting(ExceptionCollector(exception), false) } + } + +} + +internal class NettyHandlerEventTest : AbstractRealNetworkHandlerTest() { + val channel = EmbeddedChannel() + val network get() = bot.network as TestNettyNH + + override val factory: NetworkHandlerFactory = + object : NetworkHandlerFactory { + override fun create(context: NetworkHandlerContext, address: SocketAddress): TestNettyNH { + return object : TestNettyNH(context, address) { + override suspend fun createConnection(decodePipeline: PacketDecodePipeline): Channel = + channel.apply { setupChannelPipeline(pipeline(), decodePipeline) } + } + } + } + + @Test + fun `BotOnlineEvent after successful logon`() = runBlockingUnit { + assertEventBroadcasts { + assertEquals(State.INITIALIZED, network.state) + bot.login() // launches a job which broadcasts the event + delay(3.seconds) + assertEquals(State.OK, network.state) + } + } + + @Test + fun `BotReloginEvent after successful reconnection`() = runBlockingUnit { + assertEventBroadcasts { + assertEquals(State.INITIALIZED, network.state) + bot.login() + bot.firstLoginSucceed = true + network.setStateConnecting() + network.resumeConnection() + delay(3.seconds) // `login` launches a job which broadcasts the event + assertEquals(State.OK, network.state) + } + } + + @Test + fun `BotOnlineEvent after successful reconnection`() = runBlockingUnit { + assertEquals(State.INITIALIZED, network.state) + bot.login() + bot.firstLoginSucceed = true + delay(3.seconds) // `login` launches a job which broadcasts the event + assertEventBroadcasts(1) { + network.setStateConnecting() + network.resumeConnection() + delay(3.seconds) + assertEquals(State.OK, network.state) + } + } + + @Test + fun `BotOfflineEvent after successful reconnection`() = runBlockingUnit { + assertEquals(State.INITIALIZED, network.state) + bot.login() + bot.firstLoginSucceed = true + assertEquals(State.OK, network.state) + delay(3.seconds) // `login` launches a job which broadcasts the event + assertEventBroadcasts(1) { + network.setStateClosed() + delay(3.seconds) + assertEquals(State.CLOSED, network.state) + } + } +} \ No newline at end of file diff --git a/mirai-core/src/commonTest/kotlin/test/events.kt b/mirai-core/src/commonTest/kotlin/test/events.kt index dd063d420..0a5e89551 100644 --- a/mirai-core/src/commonTest/kotlin/test/events.kt +++ b/mirai-core/src/commonTest/kotlin/test/events.kt @@ -12,41 +12,37 @@ package net.mamoe.mirai.internal.test import kotlinx.coroutines.ExperimentalCoroutinesApi import net.mamoe.mirai.event.Event import net.mamoe.mirai.event.GlobalEventChannel -import java.util.concurrent.atomic.AtomicInteger +import java.util.concurrent.ConcurrentLinkedQueue +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract import kotlin.test.assertEquals @OptIn(ExperimentalCoroutinesApi::class) internal inline fun assertEventBroadcasts(times: Int = 1, block: () -> R): R { - val receivedEvents = AtomicInteger(0) - val listener = GlobalEventChannel.subscribeAlways { - receivedEvents.incrementAndGet() - } - try { + assertEventBroadcasts(times) { return block() - } finally { - listener.complete() - assertEquals( - times, - receivedEvents.get(), - "Expected event ${T::class.simpleName} broadcast $times time(s). But actual is ${receivedEvents.get()}." - ) } } @OptIn(ExperimentalCoroutinesApi::class) internal inline fun assertEventBroadcasts(times: Int = 1, block: () -> Unit) { - val receivedEvents = AtomicInteger(0) - val listener = GlobalEventChannel.subscribeAlways { - receivedEvents.incrementAndGet() + contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } + + val receivedEvents = ConcurrentLinkedQueue() + val listener = GlobalEventChannel.subscribeAlways { event -> + receivedEvents.add(event) } try { return block() } finally { + val actual = receivedEvents.filterIsInstance().count() listener.complete() assertEquals( times, - receivedEvents.get(), - "Expected event ${T::class.simpleName} broadcast $times time(s). But actual is ${receivedEvents.get()}." + actual, + "Expected event ${T::class.simpleName} broadcast $times time(s). " + + "But actual count is ${actual}. " + + "\nAll received events: ${receivedEvents.joinToString(", ", "[", "]")}" ) } } diff --git a/mirai-core/src/commonTest/kotlin/test/initPlatform.common.kt b/mirai-core/src/commonTest/kotlin/test/initPlatform.common.kt index b487ab37a..f39f67984 100644 --- a/mirai-core/src/commonTest/kotlin/test/initPlatform.common.kt +++ b/mirai-core/src/commonTest/kotlin/test/initPlatform.common.kt @@ -9,6 +9,8 @@ package net.mamoe.mirai.internal.test +import org.junit.jupiter.api.Test + internal expect fun initPlatform() /** @@ -18,4 +20,9 @@ abstract class AbstractTest { init { initPlatform() } +} + +internal expect class PlatformInitializationTest() : AbstractTest { + @Test + fun test() } \ No newline at end of file diff --git a/mirai-core/src/jvmTest/kotlin/test/initPlatform.kt b/mirai-core/src/jvmTest/kotlin/test/initPlatform.kt index 2bfae352a..47a7f1660 100644 --- a/mirai-core/src/jvmTest/kotlin/test/initPlatform.kt +++ b/mirai-core/src/jvmTest/kotlin/test/initPlatform.kt @@ -9,6 +9,16 @@ package net.mamoe.mirai.internal.test +import org.junit.jupiter.api.Test + internal actual fun initPlatform() { // nothing to do +} + + +internal actual class PlatformInitializationTest : AbstractTest() { + @Test + actual fun test() { + // nop + } } \ No newline at end of file