Compare commits

..
9 Commits
17 changed files with 300 additions and 106 deletions
+4 -1
View File
@@ -1,7 +1,10 @@
[*.{kt,kts}]
ktlint_class_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than = 4 # don't force multiline constructors unless they're big
ktlint_standard_class-naming = disabled # don't enforce capitalisation of classes
ktlint_standard_when-entry-bracing = disabled
ktlint_standard_blank-line-between-when-conditions = disabled
ktlint_standard_blank-line-before-declaration = disabled
ktlint_standard_multiline-expression-wrapping = disabled
ktlint_standard_multiline-loop = disabled
ktlint_function_signature_body_expression_wrapping = default
ktlint_standard_property-naming = disabled
ktlint_standard_function-signature = disabled
+1 -1
View File
@@ -44,6 +44,6 @@ bin/
### Mac OS ###
.DS_Store
src/test/resources/logs/ngircd.log
src/test/resources/logs/*.log
!src/test/resources/logs/.gitkeep
.idea
+5 -1
View File
@@ -14,12 +14,14 @@ repositories {
dependencies {
testImplementation(kotlin("test"))
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.11.0")
implementation("io.ktor:ktor-client-core:$ktorVersion")
implementation("io.ktor:ktor-client-cio:$ktorVersion")
implementation("io.ktor:ktor-network:$ktorVersion")
implementation("io.ktor:ktor-network-tls:$ktorVersion")
implementation("io.github.oshai:kotlin-logging-jvm:8.0.4")
implementation("ch.qos.logback:logback-classic:1.6.1")
implementation("ch.qos.logback:logback-classic:1.6.3")
testImplementation("org.jline:jansi-core:4.4.3")
}
kotlin {
@@ -28,4 +30,6 @@ kotlin {
tasks.test {
useJUnitPlatform()
val logDir = layout.projectDirectory.dir("src/test/resources/logs").toString()
systemProperty("TEST_LOG_DIR", logDir)
}
@@ -1,27 +1,39 @@
package com.shr4pnel.ferretirc
import com.shr4pnel.ferretirc.irc.Server
import com.shr4pnel.ferretirc.util.Helpers
import com.shr4pnel.ferretirc.irc.User
import com.shr4pnel.ferretirc.net.Connection
import com.shr4pnel.ferretirc.net.messages.ClientMessage
import com.shr4pnel.ferretirc.net.messages.ServerMessage
import io.github.oshai.kotlinlogging.KotlinLogging
import io.github.oshai.kotlinlogging.KotlinLoggingConfiguration
import io.github.oshai.kotlinlogging.slf4j.logger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import org.slf4j.helpers.NOPLogger
class IrcClient(hostname: String, port: Int, enableLogging: Boolean = true) {
class IrcClient(
hostname: String,
port: Int,
var enableLogging: Boolean = false,
) {
init {
KotlinLoggingConfiguration.logStartupMessage = false
Helpers.enableLogging = enableLogging
}
private val logger = if (enableLogging) KotlinLogging.logger {} else KotlinLogging.logger(NOPLogger.NOP_LOGGER)
val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
lateinit var server: Server
private set
lateinit var currentUser: User
private set
@PublishedApi
internal val connection = Connection(hostname, port, scope)
internal val connection = Connection(hostname, port, scope, enableLogging)
/**
* Connect to the socket
@@ -58,17 +70,43 @@ class IrcClient(hostname: String, port: Int, enableLogging: Boolean = true) {
}
/**
* Return the next Server Message meeting condition
* Return the next Server Message meeting given condition "predicate"
* @param predicate A function to filter a message based on its parameters
* @param T A ServerMessage inheritor
* @return An instance of ServerMessage
* @see com.shr4pnel.ferretirc.irc.Server.waitForNext
* @see com.shr4pnel.ferretirc.net.messages.ServerMessage
*/
suspend inline fun <reified T : ServerMessage> waitForNext(noinline predicate: (T) -> Boolean = { true }): T =
connection.reader.waitForNext(T::class, predicate)
server.waitForNext(T::class, predicate)
/**
* Queue a client message, and wait for the next server message meeting condition (predicate)
* @param predicate A function to filter a message based on its parameters
* @see com.shr4pnel.ferretirc.IrcClient.waitForNext
*/
suspend inline fun <reified T : ServerMessage> queueAndWaitForNext(
message: ClientMessage,
noinline predicate: (T) -> Boolean = { true },
): T = server.waitForNext(T::class, predicate) { queueMessage(message) }
/**
* Create a subscription to the server message buffer, after performing an action
* @param predicate A function to filter the message buffer flow
* @param action A function to execute after message buffer flow collection has begun
* @see com.shr4pnel.ferretirc.IrcClient.waitForNext
*/
suspend inline fun <reified T : ServerMessage> waitForNextAfterAction(
noinline predicate: (T) -> Boolean = { true },
noinline action: suspend () -> Unit,
): T = server.waitForNext(T::class, predicate, action)
suspend fun register(
nick: String,
realName: String? = null,
password: String? = null,
) {
currentUser = User(nick, "") // TODO TEMP USER ASSIGNMENT
val messages =
buildList {
if (!password.isNullOrEmpty()) add(ClientMessage.Pass(password))
@@ -80,7 +118,7 @@ class IrcClient(hostname: String, port: Int, enableLogging: Boolean = true) {
add(ClientMessage.User(nick, nick))
}
add(ClientMessage.Cap.END())
}
queueMessages(*messages.toTypedArray())
}.toTypedArray()
queueMessages(*messages)
}
}
@@ -2,38 +2,59 @@ package com.shr4pnel.ferretirc.irc
import com.shr4pnel.ferretirc.net.messages.ClientMessage
import com.shr4pnel.ferretirc.net.messages.ServerMessage
import kotlinx.coroutines.CoroutineScope
import io.github.oshai.kotlinlogging.KotlinLogging
import io.github.oshai.kotlinlogging.slf4j.logger
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onSubscription
import kotlinx.coroutines.flow.takeWhile
import org.slf4j.helpers.NOPLogger
import kotlin.reflect.KClass
import kotlin.reflect.cast
class Server(
private val msgBuffer: SharedFlow<ServerMessage>,
private val outgoingMessages: Channel<ClientMessage>,
private val scope: CoroutineScope,
enableLogging: Boolean = false,
) {
private val logger = if (enableLogging) KotlinLogging.logger {} else KotlinLogging.logger(NOPLogger.NOP_LOGGER)
lateinit var features: ISupportFeatures
private set
var channels: Set<IRCChannel> = setOf()
private set
suspend fun fetchChannels(): Set<IRCChannel> {
val buffer = mutableSetOf<IRCChannel>()
val users = mutableListOf<User>()
suspend fun fetchChannels(): Set<IRCChannel> {
logger.debug { "Fetching IRC channels" }
val buffer = mutableSetOf<IRCChannel>()
msgBuffer
.onSubscription { outgoingMessages.send(ClientMessage.List()) }
.filterIsInstance<ServerMessage.Numeric>()
.takeWhile { it !is ServerMessage.Numeric.RPL_LISTEND }
.filterIsInstance<ServerMessage.Numeric.RPL_LIST>()
.collect { buffer.add(IRCChannel(it.chanName, it.clientCount, it.topic)) }
channels = buffer.toSortedSet(compareBy { it.chanName })
return channels
}
fun fetchFeatures() {
}
suspend fun <T : ServerMessage> waitForNext(
kClass: KClass<T>,
predicate: (T) -> Boolean = { true },
onSuscribedLambda: suspend () -> Unit = {},
): T =
msgBuffer
.onSubscription { onSuscribedLambda() }
.filter { kClass.isInstance(it) }
.map { kClass.cast(it) }
.first(predicate)
}
@@ -0,0 +1,3 @@
package com.shr4pnel.ferretirc.irc
class User(val nick: String, var mask: String?)
@@ -7,7 +7,12 @@ import io.ktor.network.sockets.aSocket
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
class Connection(val hostname: String, val port: Int, val scope: CoroutineScope) {
class Connection(
val hostname: String,
val port: Int,
val scope: CoroutineScope,
val enableLogging: Boolean = false,
) {
private val selectorManager = SelectorManager(Dispatchers.IO)
private val socketBuilder = aSocket(selectorManager).tcp()
lateinit var socket: Socket
@@ -16,10 +21,10 @@ class Connection(val hostname: String, val port: Int, val scope: CoroutineScope)
suspend fun connect(): Server {
socket = socketBuilder.connect(hostname, port)
reader = MessageReader(socket, scope)
writer = MessageWriter(socket, scope)
reader = MessageReader(socket, scope, enableLogging)
writer = MessageWriter(socket, scope, enableLogging)
reader.start()
writer.start()
return Server(reader.sharedMessageBuffer, writer.outgoingMessages, scope)
return Server(reader.sharedMessageBuffer, writer.outgoingMessages, enableLogging)
}
}
@@ -1,15 +1,14 @@
package com.shr4pnel.ferretirc.net
import com.shr4pnel.ferretirc.net.messages.ServerMessage
import com.shr4pnel.ferretirc.util.Helpers
import io.github.oshai.kotlinlogging.KotlinLogging
import io.github.oshai.kotlinlogging.slf4j.logger
import kotlinx.coroutines.channels.Channel
import org.slf4j.helpers.NOPLogger
class MessageParser(val incoming: Channel<String>) {
class MessageParser(val incoming: Channel<String>, enableLogging: Boolean = false) {
val incomingParsedMessages = Channel<ServerMessage>()
private val logger = if (Helpers.enableLogging) KotlinLogging.logger {} else KotlinLogging.logger(NOPLogger.NOP_LOGGER)
private val logger = if (enableLogging) KotlinLogging.logger {} else KotlinLogging.logger(NOPLogger.NOP_LOGGER)
class MessageBuilder {
private var tags: String? = null
@@ -154,7 +153,7 @@ class MessageParser(val incoming: Channel<String>) {
suspend fun start() {
val builder = MessageBuilder()
for (msg in incoming) {
logger.trace { "Receiving: $msg" }
logger.debug { "Receiving: $msg" }
incomingParsedMessages.send(builder.build(msg))
}
}
@@ -2,7 +2,6 @@ package com.shr4pnel.ferretirc.net
import com.shr4pnel.ferretirc.net.messages.MessageIO
import com.shr4pnel.ferretirc.net.messages.ServerMessage
import com.shr4pnel.ferretirc.util.Helpers
import io.github.oshai.kotlinlogging.KotlinLogging
import io.github.oshai.kotlinlogging.slf4j.logger
import io.ktor.network.sockets.Socket
@@ -12,28 +11,23 @@ import io.ktor.utils.io.readLineStrict
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import org.slf4j.helpers.NOPLogger
import kotlin.reflect.KClass
import kotlin.reflect.cast
class MessageReader(socket: Socket, scope: CoroutineScope) : MessageIO(socket, scope) {
class MessageReader(socket: Socket, scope: CoroutineScope, enableLogging: Boolean = false) : MessageIO(socket, scope) {
private lateinit var receive: ByteReadChannel
private val messageBuffer = MutableSharedFlow<ServerMessage>(16, 64)
private val messageBuffer = MutableSharedFlow<ServerMessage>(0, 64)
val sharedMessageBuffer = messageBuffer.asSharedFlow()
val parser = MessageParser(incomingMessages)
private val logger = if (Helpers.enableLogging) KotlinLogging.logger {} else KotlinLogging.logger(NOPLogger.NOP_LOGGER)
val parser = MessageParser(incomingMessages, enableLogging)
private val logger = if (enableLogging) KotlinLogging.logger {} else KotlinLogging.logger(NOPLogger.NOP_LOGGER)
init {
scope.launch {
parser.start()
}
scope.launch {
for (msg in parser.incomingParsedMessages)
messageBuffer.emit(msg)
for (msg in parser.incomingParsedMessages) // receive parsed messages
messageBuffer.emit(msg) // send messages down shared flow
}
}
@@ -42,21 +36,8 @@ class MessageReader(socket: Socket, scope: CoroutineScope) : MessageIO(socket, s
receive = socket.openReadChannel()
while (true) {
val line = receive.readLineStrict() ?: break // >:( no LineEnding option for just CRLF? charlatans...
logger.debug { "Receive: $line" }
incomingMessages.send(line)
incomingMessages.send(line) // send raw messages to parser
}
}
}
suspend fun <T : ServerMessage> waitForNext(
kClass: KClass<T>,
predicate: (T) -> Boolean = { true },
): T {
return messageBuffer
.filter {
kClass.isInstance(it)
}.map {
kClass.cast(it)
}.first(predicate)
}
}
@@ -1,7 +1,6 @@
package com.shr4pnel.ferretirc.net
import com.shr4pnel.ferretirc.net.messages.MessageIO
import com.shr4pnel.ferretirc.util.Helpers
import io.github.oshai.kotlinlogging.KotlinLogging
import io.github.oshai.kotlinlogging.slf4j.logger
import io.ktor.network.sockets.Socket
@@ -12,9 +11,9 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.slf4j.helpers.NOPLogger
class MessageWriter(socket: Socket, scope: CoroutineScope) : MessageIO(socket, scope) {
class MessageWriter(socket: Socket, scope: CoroutineScope, enableLogging: Boolean = false) : MessageIO(socket, scope) {
private lateinit var send: ByteWriteChannel
private val logger = if (Helpers.enableLogging) KotlinLogging.logger {} else KotlinLogging.logger(NOPLogger.NOP_LOGGER)
private val logger = if (enableLogging) KotlinLogging.logger {} else KotlinLogging.logger(NOPLogger.NOP_LOGGER)
override fun start() =
scope.launch {
@@ -19,6 +19,8 @@ sealed class ClientMessage(val strName: String) {
}
}
override fun toString() = "$strName[params=$params trailing=:$trailing]"
class Ping(val token: String? = null) : ClientMessage("PING") {
init {
require(token?.isNotEmpty() ?: true) {
@@ -97,12 +99,13 @@ sealed class ClientMessage(val strName: String) {
get() = listOf(channel.chanName)
}
// TODO RPL_ISUPPORT REQUIREMENTS IN FUN EG LINELEN STATUSMSG - ALSO ADD FUCKING STUPID STATUSMSG & SUPPORT FOR MASKS. FUCK ME
class PrivMsg(val target: IRCChannel, val message: String) : ClientMessage("PRIVMSG") {
override val params: kotlin.collections.List<String>
get() =
listOf(
target.chanName,
) // TODO RPL_ISUPPORT REQUIREMENTS IN FUN EG LINELEN STATUSMSG - ALSO ADD FUCKING STUPID STATUSMSG & SUPPORT FOR MASKS. FUCK ME
)
override val trailing: String
get() = message
@@ -1,7 +0,0 @@
package com.shr4pnel.ferretirc.util
class Helpers {
companion object LoggingConfig {
var enableLogging = true
}
}
@@ -0,0 +1,109 @@
package com.shr4pnel.ferretirc.util
import ch.qos.logback.classic.Level
import ch.qos.logback.classic.spi.ILoggingEvent
import ch.qos.logback.core.pattern.CompositeConverter
import ch.qos.logback.core.pattern.color.BoldWhiteCompositeConverter
import ch.qos.logback.core.pattern.color.ForegroundCompositeConverterBase
import ch.qos.logback.core.pattern.color.ANSIConstants as ANSI
private fun center(severity: String, width: Int = 7): String {
val pad = width - severity.length
if (pad <= 0) return severity
val leading = pad / 2
return buildString {
repeat(leading) { append(" ") }
append(severity)
repeat(pad - leading) { append(" ") }
}
}
/** 8 bit ANSI codes (logback uses 3/4 bit which isn't always equivalent) */
object Codes {
const val SET_DEFAULT_COLOR = ANSI.ESC_START + ANSI.RESET + ANSI.DEFAULT_FG + ANSI.ESC_END
const val FG_START = ANSI.ESC_START + "38;5;"
const val FG_BOLD_START = ANSI.ESC_START + ANSI.BOLD + "38;5;"
const val BG_START = ANSI.ESC_START + "48;5;"
const val RED = "9"
const val BLACK = "16"
const val GREEN = "22"
const val BLUE = "27"
const val ORANGE = "202"
const val DIMMED_WHITE = "250"
const val WHITE = "255"
const val UNSET = ""
}
/**
* Interface of HighlightingCompositeConverterExt, used to colour the background and foreground of log messages
* depending on their severity.
*
* Inheritor of ForegroundCompositeConverterBase, the implementation controlling colour-based transformations of
* log events
*
* @see HighlightingCompositeConverterExt
* @see CompositeConverter
*/
abstract class ForegroundBackgroundCompositeConverter<E : ILoggingEvent> : ForegroundCompositeConverterBase<E>() {
override fun transform(event: E, `in`: String) = buildString {
val fg = getForegroundColorCode(event)
val bg = getBackgroundColourCode(event)
val severity = center(`in`.trim())
if (fg.isNotEmpty()) {
append(Codes.FG_BOLD_START)
append(fg)
append(ANSI.ESC_END)
}
if (bg.isNotEmpty()) {
append(Codes.BG_START)
append(getBackgroundColourCode(event))
append(ANSI.ESC_END)
}
append(severity)
append(Codes.SET_DEFAULT_COLOR)
}
abstract fun getBackgroundColourCode(event: ILoggingEvent): String
}
/**
* Converter class extension for control over backgrounds and foregrounds<
*
* Overrides methods of ForegroundBackgroundCompositeConverter to specify the bg/fg colours depending on severity
*
* Adapted from [shuwada/logback-custom-color](https://github.com/shuwada/logback-custom-color)
*/
class HighlightingCompositeConverterExt : ForegroundBackgroundCompositeConverter<ILoggingEvent>() {
override fun getForegroundColorCode(event: ILoggingEvent) = when (event.level) {
Level.TRACE -> Codes.WHITE
Level.DEBUG -> Codes.WHITE
Level.WARN -> Codes.WHITE
Level.INFO -> Codes.WHITE
Level.ERROR -> Codes.WHITE
else -> Codes.UNSET
}
override fun getBackgroundColourCode(event: ILoggingEvent) = when (event.level) {
Level.TRACE -> Codes.BLACK
Level.DEBUG -> Codes.GREEN
Level.WARN -> Codes.ORANGE
Level.INFO -> Codes.BLUE
Level.ERROR -> Codes.RED
else -> Codes.UNSET
}
}
/**
* Converter rule which pulls out the last string of the thread split by spaces
*/
class ThreadConverter<E> : CompositeConverter<E>() {
override fun transform(event: E, `in`: String) = `in`.split(" ").last()
}
/**
* Converter rule which converts text to bold white
*/
class BoldWhiteCompositeConverterExt<E> : BoldWhiteCompositeConverter<E>() {
override fun transform(event: E, `in`: String) =
Codes.FG_BOLD_START + Codes.DIMMED_WHITE + ANSI.ESC_END + `in` + Codes.SET_DEFAULT_COLOR
}
+29
View File
@@ -0,0 +1,29 @@
<included>
<import class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"/>
<import class="ch.qos.logback.core.ConsoleAppender"/>
<import class="ch.qos.logback.core.FileAppender"/>
<import class="com.shr4pnel.ferretirc.util.HighlightingCompositeConverterExt"/>
<import class="com.shr4pnel.ferretirc.util.ThreadConverter"/>
<import class="com.shr4pnel.ferretirc.util.BoldWhiteCompositeConverterExt"/>
<import class="ch.qos.logback.classic.filter.ThresholdFilter"/>
<!-- https://logback.qos.ch/manual/layouts.html#formatModifiers -->
<conversionRule conversionWord="highlightext" class="HighlightingCompositeConverterExt" />
<conversionRule conversionWord="boldwhiteext" class="BoldWhiteCompositeConverterExt" />
<conversionRule conversionWord="threadconvert" class="ThreadConverter" />
<appender name="STDOUT" class="ConsoleAppender">
<filter class="ThresholdFilter">
<level>${STDOUT_LEVEL:-debug}</level>
</filter>
<encoder class="PatternLayoutEncoder">
<pattern>%d{HH:mm:ss.SSS} %highlightext(%level) %-13.13threadconvert(%t) %-36.36logger %boldwhiteext(%msg) %n</pattern>
</encoder>
</appender>
<appender name="FILE" class="FileAppender">
<file>${TEST_LOG_DIR}/libferretirc.log</file>
<immediateFlush>true</immediateFlush>
<append>false</append>
<encoder class="PatternLayoutEncoder">
<pattern>%d{HH:mm:ss.SSS} [%.16t/%-5level] %logger %msg%n</pattern>
</encoder>
</appender>
</included>
+2 -8
View File
@@ -2,14 +2,8 @@
<!DOCTYPE configuration>
<!-- From https://logback.qos.ch/manual/configuration.html -->
<configuration>
<import class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"/>
<import class="ch.qos.logback.core.ConsoleAppender"/>
<appender name="STDOUT" class="ConsoleAppender">
<encoder class="PatternLayoutEncoder">
<pattern>%d{ISO8601} [%5level] %.36logger %msg%n</pattern>
</encoder>
</appender>
<root level="debug">
<include resource="logback-base.xml"/>
<root level="info">
<appender-ref ref="STDOUT"/>
</root>
</configuration>
@@ -2,17 +2,14 @@ package com.shr4pnel.ferretirc
import com.shr4pnel.ferretirc.net.messages.ServerMessage
import io.github.oshai.kotlinlogging.KotlinLogging
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeoutOrNull
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import java.io.File
import java.lang.ProcessBuilder
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.time.Duration.Companion.seconds
import com.shr4pnel.ferretirc.net.messages.ClientMessage as Message
class IrcClientTest {
@@ -20,12 +17,13 @@ class IrcClientTest {
companion object {
lateinit var processes: MutableList<Process>
val client = IrcClient("localhost", 6667)
val client = IrcClient("localhost", 6667, enableLogging = true)
private val logger = KotlinLogging.logger {}
val guestUser = IrcClient("localhost", 6667)
@JvmStatic
@BeforeAll
fun setup() =
fun setup(): Unit =
runBlocking {
val logFile = File("src/test/resources/logs/ngircd.log")
val ngircdConfigPath = javaClass.classLoader.getResource("ngircd.conf")!!.path
@@ -42,7 +40,10 @@ class IrcClientTest {
),
)
client.connect()
client.register("shr4p", "Tyler D", "password")
client.register("shr4p", "Tyler Fullname", "password")
guestUser.connect()
guestUser.register("guest", "guest", "pass")
client.server
}
@JvmStatic
@@ -56,38 +57,20 @@ class IrcClientTest {
}
}
@Test
fun sample() {
client.scope.launch {
}
}
@Test
fun pingGetsPong() =
runBlocking {
runTest {
val token = "ACK"
logger.info { "Sending PING $token" }
client.queueMessage(Message.Ping(token))
logger.info { "Waiting to receive PONG" }
val pong =
withTimeoutOrNull(1.seconds) {
client.waitForNext<ServerMessage.Pong> { it.token.equals(token) }
}
assertNotNull(pong, "Reached timeout while waiting for PONG")
val msg = Message.Ping(token)
val pong = client.queueAndWaitForNext<ServerMessage.Pong>(msg)
assertEquals(token, pong.token, "Token in PING did not match PONG")
logger.info { "Received PING" }
}
@Test
fun oper() =
runBlocking {
runTest {
client.queueMessage(Message.Oper("shr4p", "password"))
val mode =
withTimeoutOrNull(1.seconds) {
client.waitForNext<ServerMessage.Mode>()
}
assertNotNull(mode, "Timed out waiting for OPER MODE response")
val mode = client.waitForNext<ServerMessage.Mode>()
assertEquals("shr4p", mode.operatorName, "Received incorrect operator name in MODE")
assertEquals("+o", mode.pMask, "Received unexpected mask in MODE")
logger.info { "Received MODE with mask ${mode.pMask}" }
@@ -95,7 +78,7 @@ class IrcClientTest {
@Test
fun listChannels() =
runBlocking {
runTest {
val channels = client.server.fetchChannels()
assert(channels.isNotEmpty())
client.queueMessage(Message.Join(channels.first()))
@@ -103,10 +86,7 @@ class IrcClientTest {
@Test
fun messageLands() =
runBlocking {
val guestUser = IrcClient("localhost", 6667, false)
guestUser.connect()
guestUser.register("guest", "guest", "pass")
runTest {
val channels = client.server.fetchChannels()
val message = Message.PrivMsg(channels.first(), "bring me to life")
guestUser.queueMessage(message)
@@ -114,6 +94,24 @@ class IrcClientTest {
logger.debug { "Message: $message, Received: $receivedMessage" }
assertEquals(message.message, receivedMessage.message)
assertEquals(message.target.chanName, receivedMessage.targets.first())
guestUser.close()
}
@Test
fun manyMessagesLand() =
runTest {
val channels = client.server.fetchChannels()
val channel = channels.first()
val max = 100
val messages = mutableListOf<Message.PrivMsg>()
val receivedMessages = mutableListOf<ServerMessage.PrivMsg>()
for (i in 1..max) messages.add(Message.PrivMsg(channel, "$i: bring me to life"))
for (i in 0..<max) {
val msg = client.waitForNextAfterAction<ServerMessage.PrivMsg> { guestUser.queueMessage(messages[i]) }
receivedMessages.add(msg)
}
for (i in 0..<max) assertEquals(messages[i].message, receivedMessages[i].message)
}
}
+15
View File
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration>
<!-- From https://logback.qos.ch/manual/configuration.html -->
<configuration>
<import class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"/>
<import class="ch.qos.logback.core.ConsoleAppender"/>
<include resource="logback-base.xml"/>
<logger name="com.shr4pnel" level="trace" additivity="false">
<appender-ref ref="STDOUT"/>
<appender-ref ref="FILE"/>
</logger>
<root level="warn">
<appender-ref ref="STDOUT"/>
</root>
</configuration>