Add NetworkHandlerFactory and tests for NetworkHandler

This commit is contained in:
Him188 2021-04-24 14:13:08 +08:00
parent 4f77abca87
commit b91bbfd2b8
19 changed files with 472 additions and 117 deletions

View File

@ -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,

View File

@ -12,7 +12,10 @@ package net.mamoe.mirai.utils
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
public class ExceptionCollector {
public class ExceptionCollector : Sequence<Throwable> {
// 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<Throwable> {
return (sequenceOf(this) + this.suppressed.asSequence().flatMap { it.itr().asSequence() }).iterator()
}
override fun iterator(): Iterator<Throwable> {
val last = getLast() ?: return emptyList<Throwable>().iterator()
return last.itr()
}
}
/**

View File

@ -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
}
}
}

View File

@ -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()) {
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 {

View File

@ -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)
private val SHOW_ALL_COMPONENTS: Boolean by lazy(NONE) { systemProp("mirai.debug.network.show.all.components", false) }

View File

@ -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()

View File

@ -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<SsoProcessor>
}
@ -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(

View File

@ -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>
/**
* 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<H : NetworkHandler> {
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
}

View File

@ -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<H : NetworkHandler> {
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
}

View File

@ -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<NettyChannel>()
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<RawIncomingPacket> = 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 {

View File

@ -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())

View File

@ -31,11 +31,20 @@ import java.net.InetSocketAddress
/**
* With real factory and components as in [QQAndroidBot.components].
*/
internal abstract class AbstractRealNetworkHandlerTest<H : NetworkHandler>(
private val factory: NetworkHandlerFactory<H>,
) : AbstractTest() {
val bot = MockBot()
val networkLogger = MiraiLogger.TopLevel
internal abstract class AbstractRealNetworkHandlerTest<H : NetworkHandler> : 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<H>
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<H : NetworkHandler>(
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<H : NetworkHandler>(
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,

View File

@ -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> {
TestEvent().broadcast()
}
}
@Test
fun `observes expected event`() = runBlockingUnit {
assertEventBroadcasts<TestEvent>(1) {
TestEvent().broadcast()
TestEvent2().broadcast()
}
}
@Test
fun `can observe event multiple times`() = runBlockingUnit {
assertEventBroadcasts<TestEvent>(2) {
TestEvent().broadcast()
TestEvent().broadcast()
}
}
@Test
fun `can observe event only in block`() = runBlockingUnit {
TestEvent().broadcast()
assertEventBroadcasts<TestEvent>(1) {
TestEvent().broadcast()
}
}
@Test
fun `fails if times not match`() = runBlockingUnit {
assertFails {
assertEventBroadcasts<TestEvent>(2) {
TestEvent().broadcast()
}
}
assertFails {
assertEventBroadcasts<TestEvent>(0) {
TestEvent().broadcast()
}
}
}
}

View File

@ -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<NettyNetworkHandler>(NettyNetworkHandlerFactory) {
@Test
fun `BotOnlineEvent after successful logon`() = runBlockingUnit {
bot.login()
}
}

View File

@ -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<Pair<NetworkHandlerSupport.BaseStateImpl, NetworkHandlerSupport.BaseStateImpl>>()
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)
}
}

View File

@ -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<TestNettyNH>() {
val channel = EmbeddedChannel()
val network get() = bot.network as TestNettyNH
override val factory: NetworkHandlerFactory<TestNettyNH> =
object : NetworkHandlerFactory<TestNettyNH> {
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<BotOnlineEvent> {
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<BotReloginEvent> {
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<BotOnlineEvent>(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<BotOfflineEvent>(1) {
network.setStateClosed()
delay(3.seconds)
assertEquals(State.CLOSED, network.state)
}
}
}

View File

@ -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 <reified T : Event, R> assertEventBroadcasts(times: Int = 1, block: () -> R): R {
val receivedEvents = AtomicInteger(0)
val listener = GlobalEventChannel.subscribeAlways<T> {
receivedEvents.incrementAndGet()
}
try {
assertEventBroadcasts<T>(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 <reified T : Event> assertEventBroadcasts(times: Int = 1, block: () -> Unit) {
val receivedEvents = AtomicInteger(0)
val listener = GlobalEventChannel.subscribeAlways<T> {
receivedEvents.incrementAndGet()
contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
val receivedEvents = ConcurrentLinkedQueue<Event>()
val listener = GlobalEventChannel.subscribeAlways<Event> { event ->
receivedEvents.add(event)
}
try {
return block()
} finally {
val actual = receivedEvents.filterIsInstance<T>().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(", ", "[", "]")}"
)
}
}

View File

@ -9,6 +9,8 @@
package net.mamoe.mirai.internal.test
import org.junit.jupiter.api.Test
internal expect fun initPlatform()
/**
@ -19,3 +21,8 @@ abstract class AbstractTest {
initPlatform()
}
}
internal expect class PlatformInitializationTest() : AbstractTest {
@Test
fun test()
}

View File

@ -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
}
}