Browse Source

Better server state handling

Expose status (connecting, negotiating, ready)
Fire ServerReady and ServerConnected events
tags/v0.3.0
Chris Smith 5 years ago
parent
commit
812ec4990b

+ 2
- 0
CHANGELOG View File

@@ -7,6 +7,8 @@ vNEXT (in development)
7 7
   * Server state improvements:
8 8
     * Added status field to ServerState
9 9
     * ServerConnected event is emitted as soon as the socket is connected
10
+    * ServerReady event is emitted after logging in, negotiating, etc
11
+  * (Internal) Event handlers can now return more events to emit
10 12
 
11 13
 v0.2.1
12 14
 

+ 3
- 0
src/itest/kotlin/com/dmdirc/irctest/TestRunner.kt View File

@@ -30,6 +30,7 @@ class IrcLibraryTests {
30 30
             val clientInput = BufferedReader(InputStreamReader(clientSocket.getInputStream()))
31 31
             val clientOutput = BufferedWriter(OutputStreamWriter(clientSocket.getOutputStream()))
32 32
             for (step in test.steps) {
33
+                println(step)
33 34
                 when (step) {
34 35
                     is SimpleExpectStep -> {
35 36
                         while (true) {
@@ -49,6 +50,8 @@ class IrcLibraryTests {
49 50
                     }
50 51
                 }
51 52
             }
53
+
54
+            library.terminate()
52 55
         }
53 56
     }
54 57
 

+ 7
- 1
src/itest/kotlin/com/dmdirc/ktirc/KtIrcIntegrationTest.kt View File

@@ -3,6 +3,8 @@ package com.dmdirc.ktirc
3 3
 import com.dmdirc.irctest.IrcLibraryTests
4 4
 import com.dmdirc.ktirc.model.Profile
5 5
 import com.dmdirc.ktirc.model.Server
6
+import kotlinx.coroutines.delay
7
+import kotlinx.coroutines.runBlocking
6 8
 import org.junit.jupiter.api.TestFactory
7 9
 
8 10
 class KtIrcIntegrationTest {
@@ -18,7 +20,11 @@ class KtIrcIntegrationTest {
18 20
         }
19 21
 
20 22
         override fun terminate() {
21
-            ircClient.disconnect()
23
+            runBlocking {
24
+                delay(100)
25
+                ircClient.disconnect()
26
+                ircClient.join()
27
+            }
22 28
         }
23 29
 
24 30
     })

+ 10
- 4
src/main/kotlin/com/dmdirc/ktirc/IrcClient.kt View File

@@ -100,6 +100,7 @@ class IrcClientImpl(private val server: Server, private val profile: Profile) :
100 100
     private var connectionJob: Job? = null
101 101
 
102 102
     override fun send(message: String) {
103
+        // TODO: What happens if sending fails?
103 104
         scope.launch {
104 105
             socket?.sendLine(message)
105 106
         }
@@ -112,9 +113,9 @@ class IrcClientImpl(private val server: Server, private val profile: Profile) :
112 113
                 // TODO: Proper error handling - what if connect() fails?
113 114
                 socket = this
114 115
                 connect()
115
-                messageHandler.emitEvent(this@IrcClientImpl, ServerConnected(currentTimeProvider()))
116
-                sendLine("CAP LS 302") // TODO: Stick this in a builder
117
-                server.password?.let { pass -> sendPassword(pass) }
116
+                emitEvent(ServerConnected(currentTimeProvider()))
117
+                sendCapabilityList()
118
+                sendPasswordIfPresent()
118 119
                 sendNickChange(profile.initialNick)
119 120
                 // TODO: Send correct host
120 121
                 sendUser(profile.userName, "localhost", server.host, profile.realName)
@@ -133,11 +134,16 @@ class IrcClientImpl(private val server: Server, private val profile: Profile) :
133 134
 
134 135
     override fun onEvent(handler: (IrcEvent) -> Unit) {
135 136
         messageHandler.handlers.add(object : EventHandler {
136
-            override fun processEvent(client: IrcClient, event: IrcEvent) {
137
+            override fun processEvent(client: IrcClient, event: IrcEvent): List<IrcEvent> {
137 138
                 handler(event)
139
+                return emptyList()
138 140
             }
139 141
         })
140 142
     }
143
+
144
+    private fun emitEvent(event: IrcEvent) = messageHandler.emitEvent(this, event)
145
+    private fun sendPasswordIfPresent() = server.password?.let(this::sendPassword)
146
+
141 147
 }
142 148
 
143 149
 internal fun main() {

+ 2
- 1
src/main/kotlin/com/dmdirc/ktirc/events/CapabilitiesHandler.kt View File

@@ -12,12 +12,13 @@ internal class CapabilitiesHandler : EventHandler {
12 12
 
13 13
     private val log by logger()
14 14
 
15
-    override fun processEvent(client: IrcClient, event: IrcEvent) {
15
+    override fun processEvent(client: IrcClient, event: IrcEvent): List<IrcEvent> {
16 16
         when (event) {
17 17
             is ServerCapabilitiesReceived -> handleCapabilitiesReceived(client.serverState.capabilities, event.capabilities)
18 18
             is ServerCapabilitiesFinished -> handleCapabilitiesFinished(client)
19 19
             is ServerCapabilitiesAcknowledged -> handleCapabilitiesAcknowledged(client, event.capabilities)
20 20
         }
21
+        return emptyList()
21 22
     }
22 23
 
23 24
     private fun handleCapabilitiesReceived(state: CapabilitiesState, capabilities: Map<Capability, String>) {

+ 2
- 1
src/main/kotlin/com/dmdirc/ktirc/events/ChannelStateHandler.kt View File

@@ -9,7 +9,7 @@ internal class ChannelStateHandler : EventHandler {
9 9
 
10 10
     private val log by logger()
11 11
 
12
-    override fun processEvent(client: IrcClient, event: IrcEvent) {
12
+    override fun processEvent(client: IrcClient, event: IrcEvent): List<IrcEvent> {
13 13
         when (event) {
14 14
             is ChannelJoined -> handleJoin(client, event)
15 15
             is ChannelParted -> handlePart(client, event)
@@ -17,6 +17,7 @@ internal class ChannelStateHandler : EventHandler {
17 17
             is ChannelNamesFinished -> handleNamesFinished(client, event)
18 18
             is UserQuit -> handleQuit(client, event)
19 19
         }
20
+        return emptyList()
20 21
     }
21 22
 
22 23
     private fun handleJoin(client: IrcClient, event: ChannelJoined) {

+ 1
- 1
src/main/kotlin/com/dmdirc/ktirc/events/EventHandler.kt View File

@@ -5,7 +5,7 @@ import com.dmdirc.ktirc.IrcClient
5 5
 @FunctionalInterface
6 6
 internal interface EventHandler {
7 7
 
8
-    fun processEvent(client: IrcClient, event: IrcEvent)
8
+    fun processEvent(client: IrcClient, event: IrcEvent): List<IrcEvent>
9 9
 
10 10
 }
11 11
 

+ 3
- 0
src/main/kotlin/com/dmdirc/ktirc/events/Events.kt View File

@@ -11,6 +11,9 @@ sealed class IrcEvent(val time: LocalDateTime)
11 11
 /** Raised when the connection to the server has been established. The server will not be ready for use yet. */
12 12
 class ServerConnected(time: LocalDateTime) : IrcEvent(time)
13 13
 
14
+/** Raised when the server is ready for use. */
15
+class ServerReady(time: LocalDateTime) : IrcEvent(time)
16
+
14 17
 /** Raised when the server initially welcomes us to the IRC network. */
15 18
 class ServerWelcome(time: LocalDateTime, val localNick: String) : IrcEvent(time)
16 19
 

+ 2
- 1
src/main/kotlin/com/dmdirc/ktirc/events/PingHandler.kt View File

@@ -5,10 +5,11 @@ import com.dmdirc.ktirc.messages.sendPong
5 5
 
6 6
 internal class PingHandler : EventHandler {
7 7
 
8
-    override fun processEvent(client: IrcClient, event: IrcEvent) {
8
+    override fun processEvent(client: IrcClient, event: IrcEvent): List<IrcEvent> {
9 9
         when (event) {
10 10
             is PingReceived -> client.sendPong(event.nonce)
11 11
         }
12
+        return emptyList()
12 13
     }
13 14
 
14 15
 }

+ 28
- 2
src/main/kotlin/com/dmdirc/ktirc/events/ServerStateHandler.kt View File

@@ -1,14 +1,40 @@
1 1
 package com.dmdirc.ktirc.events
2 2
 
3 3
 import com.dmdirc.ktirc.IrcClient
4
+import com.dmdirc.ktirc.model.ServerState
5
+import com.dmdirc.ktirc.model.ServerStatus
6
+import java.time.LocalDateTime
4 7
 
5 8
 internal class ServerStateHandler : EventHandler {
6 9
 
7
-    override fun processEvent(client: IrcClient, event: IrcEvent) {
10
+    override fun processEvent(client: IrcClient, event: IrcEvent): List<IrcEvent> {
8 11
         when (event) {
9
-            is ServerWelcome -> client.serverState.localNickname = event.localNick
12
+            is ServerConnected -> client.serverState.status = ServerStatus.Negotiating
13
+            is ServerWelcome -> handleWelcome(client.serverState, event.localNick)
10 14
             is ServerFeaturesUpdated -> client.serverState.features.setAll(event.serverFeatures)
15
+
16
+            // Events that won't trigger a server ready event
17
+            is PingReceived -> Unit
18
+            is ServerCapabilitiesReceived -> Unit
19
+            is ServerCapabilitiesAcknowledged -> Unit
20
+            is ServerCapabilitiesFinished -> Unit
21
+
22
+            else -> return checkReadyState(client, event.time)
23
+        }
24
+        return emptyList()
25
+    }
26
+
27
+    private fun handleWelcome(serverState: ServerState, localNick: String) {
28
+        serverState.receivedWelcome = true
29
+        serverState.localNickname = localNick
30
+    }
31
+
32
+    private fun checkReadyState(client: IrcClient, time: LocalDateTime): List<IrcEvent> {
33
+        if (client.serverState.receivedWelcome && client.serverState.status == ServerStatus.Negotiating) {
34
+            client.serverState.status = ServerStatus.Ready
35
+            return listOf(ServerReady(time))
11 36
         }
37
+        return emptyList()
12 38
     }
13 39
 
14 40
 }

+ 2
- 1
src/main/kotlin/com/dmdirc/ktirc/events/UserStateHandler.kt View File

@@ -5,13 +5,14 @@ import com.dmdirc.ktirc.model.UserState
5 5
 
6 6
 internal class UserStateHandler : EventHandler {
7 7
 
8
-    override fun processEvent(client: IrcClient, event: IrcEvent) {
8
+    override fun processEvent(client: IrcClient, event: IrcEvent): List<IrcEvent> {
9 9
         when (event) {
10 10
             is ChannelJoined -> handleJoin(client.userState, event)
11 11
             is ChannelParted -> handlePart(client, event)
12 12
             is ChannelNamesReceived  -> handleNamesReceived(client, event)
13 13
             is UserQuit -> handleQuit(client.userState, event)
14 14
         }
15
+        return emptyList()
15 16
     }
16 17
 
17 18
     private fun handleJoin(state: UserState, event: ChannelJoined) {

+ 3
- 1
src/main/kotlin/com/dmdirc/ktirc/io/MessageHandler.kt View File

@@ -20,7 +20,9 @@ internal class MessageHandler(private val processors: List<MessageProcessor>, va
20 20
 
21 21
     fun emitEvent(ircClient: IrcClient, ircEvent: IrcEvent) {
22 22
         handlers.forEach { handler ->
23
-            handler.processEvent(ircClient, ircEvent)
23
+            handler.processEvent(ircClient, ircEvent).forEach {
24
+                emitEvent(ircClient, it)
25
+            }
24 26
         }
25 27
     }
26 28
 

+ 2
- 0
src/main/kotlin/com/dmdirc/ktirc/messages/MessageBuilders.kt View File

@@ -2,6 +2,8 @@ package com.dmdirc.ktirc.messages
2 2
 
3 3
 import com.dmdirc.ktirc.IrcClient
4 4
 
5
+/** Sends a message to ask the server to list capabilities. */
6
+internal fun IrcClient.sendCapabilityList() = send("CAP LS 302")
5 7
 /** Sends a message indicating the end of capability negotiation. */
6 8
 internal fun IrcClient.sendCapabilityEnd() = send("CAP END")
7 9
 /** Sends a message requesting the specified caps are enabled. */

+ 4
- 1
src/main/kotlin/com/dmdirc/ktirc/model/ServerState.kt View File

@@ -8,6 +8,9 @@ import kotlin.reflect.KClass
8 8
  */
9 9
 class ServerState internal constructor(initialNickname: String) {
10 10
 
11
+    /** Whether we've received the 'Welcome to IRC' (001) message. */
12
+    internal var receivedWelcome = false
13
+
11 14
     /** The current status of the server. */
12 15
     var status = ServerStatus.Connecting
13 16
         internal set
@@ -72,4 +75,4 @@ enum class ServerStatus {
72 75
     Negotiating,
73 76
     /** We are connected and commands can be sent. */
74 77
     Ready,
75
-}
78
+}

+ 52
- 3
src/test/kotlin/com/dmdirc/ktirc/events/ServerStateHandlerTest.kt View File

@@ -2,13 +2,12 @@ package com.dmdirc.ktirc.events
2 2
 
3 3
 import com.dmdirc.ktirc.IrcClient
4 4
 import com.dmdirc.ktirc.TestConstants
5
-import com.dmdirc.ktirc.model.ServerFeature
6
-import com.dmdirc.ktirc.model.ServerFeatureMap
7
-import com.dmdirc.ktirc.model.ServerState
5
+import com.dmdirc.ktirc.model.*
8 6
 import com.nhaarman.mockitokotlin2.doReturn
9 7
 import com.nhaarman.mockitokotlin2.mock
10 8
 import kotlinx.coroutines.runBlocking
11 9
 import org.junit.jupiter.api.Assertions.assertEquals
10
+import org.junit.jupiter.api.Assertions.assertTrue
12 11
 import org.junit.jupiter.api.Test
13 12
 
14 13
 internal class ServerStateHandlerTest {
@@ -26,6 +25,56 @@ internal class ServerStateHandlerTest {
26 25
         assertEquals("acidBurn", serverState.localNickname)
27 26
     }
28 27
 
28
+    @Test
29
+    fun `ServerStateHandler sets receivedWelcome on welcome event`() = runBlocking {
30
+        handler.processEvent(ircClient, ServerWelcome(TestConstants.time, "acidBurn"))
31
+        assertTrue(serverState.receivedWelcome)
32
+    }
33
+
34
+    @Test
35
+    fun `ServerStateHandler sets state to negotiating on connected`() = runBlocking {
36
+        handler.processEvent(ircClient, ServerConnected(TestConstants.time))
37
+        assertEquals(ServerStatus.Negotiating, serverState.status)
38
+    }
39
+
40
+    @Test
41
+    fun `ServerStateHandler sets server state to ready on receiving post-005 line`() = runBlocking {
42
+        ircClient.serverState.status = ServerStatus.Negotiating
43
+
44
+        listOf(
45
+                ServerWelcome(TestConstants.time, "acidBurn"),
46
+                PingReceived(TestConstants.time, "1234".toByteArray()),
47
+                ServerCapabilitiesReceived(TestConstants.time, emptyMap()),
48
+                ServerCapabilitiesAcknowledged(TestConstants.time, emptyMap()),
49
+                ServerCapabilitiesFinished(TestConstants.time),
50
+                MessageReceived(TestConstants.time, User("zeroCool"), "acidBurn", "Welcome!")
51
+        ).forEach {
52
+            assertEquals(ServerStatus.Negotiating, serverState.status)
53
+            handler.processEvent(ircClient, it)
54
+        }
55
+
56
+        assertEquals(ServerStatus.Ready, serverState.status)
57
+    }
58
+
59
+    @Test
60
+    fun `ServerStateHandler emits event on receiving post-005 line`() = runBlocking {
61
+        ircClient.serverState.status = ServerStatus.Negotiating
62
+
63
+        listOf(
64
+                ServerWelcome(TestConstants.time, "acidBurn"),
65
+                PingReceived(TestConstants.time, "1234".toByteArray()),
66
+                ServerCapabilitiesReceived(TestConstants.time, emptyMap()),
67
+                ServerCapabilitiesAcknowledged(TestConstants.time, emptyMap()),
68
+                ServerCapabilitiesFinished(TestConstants.time)
69
+        ).forEach {
70
+            assertTrue(handler.processEvent(ircClient, it).isEmpty())
71
+        }
72
+
73
+        val events = handler.processEvent(ircClient, MessageReceived(TestConstants.time, User("zeroCool"), "acidBurn", "Welcome!"))
74
+        assertEquals(1, events.size)
75
+        assertTrue(events[0] is ServerReady)
76
+    }
77
+
29 78
     @Test
30 79
     fun `ServerStateHandler updates features on features event`() = runBlocking {
31 80
         val features = ServerFeatureMap()

+ 16
- 2
src/test/kotlin/com/dmdirc/ktirc/io/MessageHandlerTest.kt View File

@@ -4,6 +4,7 @@ import com.dmdirc.ktirc.IrcClient
4 4
 import com.dmdirc.ktirc.TestConstants
5 5
 import com.dmdirc.ktirc.events.EventHandler
6 6
 import com.dmdirc.ktirc.events.ServerConnected
7
+import com.dmdirc.ktirc.events.ServerReady
7 8
 import com.dmdirc.ktirc.events.ServerWelcome
8 9
 import com.dmdirc.ktirc.messages.MessageProcessor
9 10
 import com.dmdirc.ktirc.model.IrcMessage
@@ -60,7 +61,7 @@ internal class MessageHandlerTest {
60 61
     }
61 62
 
62 63
     @Test
63
-    fun `MessageHandler invokes all event handler with all returned events`() = runBlocking {
64
+    fun `MessageHandler invokes all event handler with all returned events`() = runBlocking<Unit> {
64 65
         val eventHandler1 = mock<EventHandler>()
65 66
         val eventHandler2 = mock<EventHandler>()
66 67
         val handler = MessageHandler(listOf(joinProcessor, nickProcessor), mutableListOf(eventHandler1, eventHandler2))
@@ -80,7 +81,7 @@ internal class MessageHandlerTest {
80 81
     }
81 82
 
82 83
     @Test
83
-    fun `MessageHandler emits custom events to all handlers`() = runBlocking {
84
+    fun `MessageHandler emits custom events to all handlers`() = runBlocking<Unit> {
84 85
         val eventHandler1 = mock<EventHandler>()
85 86
         val eventHandler2 = mock<EventHandler>()
86 87
         val handler = MessageHandler(emptyList(), mutableListOf(eventHandler1, eventHandler2))
@@ -90,4 +91,17 @@ internal class MessageHandlerTest {
90 91
         verify(eventHandler2).processEvent(same(ircClient), isA<ServerWelcome>())
91 92
     }
92 93
 
94
+    @Test
95
+    fun `MessageHandler emits events returned from handler`() = runBlocking<Unit> {
96
+        val eventHandler1 = mock<EventHandler> {
97
+            on { processEvent(any(), isA<ServerWelcome>()) } doReturn listOf(ServerReady(TestConstants.time))
98
+        }
99
+        val eventHandler2 = mock<EventHandler>()
100
+        val handler = MessageHandler(emptyList(), mutableListOf(eventHandler1, eventHandler2))
101
+        handler.emitEvent(ircClient, ServerWelcome(TestConstants.time, "abc"))
102
+
103
+        verify(eventHandler1).processEvent(same(ircClient), isA<ServerReady>())
104
+        verify(eventHandler2).processEvent(same(ircClient), isA<ServerReady>())
105
+    }
106
+
93 107
 }

Loading…
Cancel
Save