Archived
1
0
Fork 0

Introduce EventHandlerPositionManager with (failing) tests

This commit is contained in:
Moritz Ruth 2020-12-26 18:23:46 +01:00
parent 1f10e52131
commit 839fe1100f
No known key found for this signature in database
GPG key ID: AFD57E23E753841B
16 changed files with 270 additions and 70 deletions

View file

@ -14,10 +14,10 @@ abstract class EventEmitter<E: Event> {
*/
open fun <T: E> on(
eventType: KClass<T>,
priority: EventPriority = EventPriority.NORMAL,
handlerPosition: EventHandlerPosition = EventHandlerPosition.NORMAL,
fn: suspend (event: T) -> Unit
): EventHandler<T> {
val handler = EventHandler(this, eventType, Plugin.getCalling(), priority, fn)
val handler = EventHandler(this, eventType, Plugin.getCalling(), handlerPosition, fn)
handlers.add(handler)
return handler
}
@ -29,12 +29,12 @@ abstract class EventEmitter<E: Event> {
*/
fun <T: E> onRemovable(
eventType: KClass<T>,
priority: EventPriority = EventPriority.NORMAL,
handlerPosition: EventHandlerPosition = EventHandlerPosition.NORMAL,
fn: suspend (event: T, remove: () -> Unit) -> Unit
): EventHandler<T> {
lateinit var handler: EventHandler<T>
handler = on(eventType, priority) {
handler = on(eventType, handlerPosition) {
fn(it, handler::remove)
}
@ -46,16 +46,16 @@ abstract class EventEmitter<E: Event> {
*/
fun <T: E> once(
eventType: KClass<T>,
priority: EventPriority = EventPriority.NORMAL,
handlerPosition: EventHandlerPosition = EventHandlerPosition.NORMAL,
fn: suspend (event: T) -> Unit
): EventHandler<T> = onRemovable(eventType, priority) { event, remove -> remove(); fn(event) }
): EventHandler<T> = onRemovable(eventType, handlerPosition) { event, remove -> remove(); fn(event) }
/**
* Suspends until an event of type [T] is emitted and returns it.
*/
suspend fun <T: E> waitFor(eventType: KClass<T>, priority: EventPriority = EventPriority.NORMAL): T =
suspend fun <T: E> waitFor(eventType: KClass<T>, handlerPosition: EventHandlerPosition = EventHandlerPosition.NORMAL): T =
suspendCancellableCoroutine { c ->
val handler = EventHandler(this, eventType, Plugin.getCalling(), priority) {
val handler = EventHandler(this, eventType, Plugin.getCalling(), handlerPosition) {
c.resume(it)
}
@ -75,9 +75,9 @@ abstract class EventEmitter<E: Event> {
* Registers a new event handler.
*/
inline fun <reified T: E> on(
priority: EventPriority = EventPriority.NORMAL,
handlerPosition: EventHandlerPosition = EventHandlerPosition.NORMAL,
noinline fn: suspend (event: T) -> Unit
): EventHandler<T> = on(T::class, priority, fn)
): EventHandler<T> = on(T::class, handlerPosition, fn)
/**
* Registers a new event handler.
@ -85,28 +85,28 @@ abstract class EventEmitter<E: Event> {
* The handler function receives a function to remove the event handler as second parameter.
*/
inline fun <reified T: E> onRemovable(
priority: EventPriority = EventPriority.NORMAL,
handlerPosition: EventHandlerPosition = EventHandlerPosition.NORMAL,
noinline fn: suspend (event: T, remove: () -> Unit) -> Unit
): EventHandler<T> = onRemovable(T::class, priority, fn)
): EventHandler<T> = onRemovable(T::class, handlerPosition, fn)
/**
* Registers a new event handler which is automatically removed after it was invoked.
*/
inline fun <reified T: E> once(
priority: EventPriority = EventPriority.NORMAL,
handlerPosition: EventHandlerPosition = EventHandlerPosition.NORMAL,
noinline fn: suspend (event: T) -> Unit
): EventHandler<T> = once(T::class, priority, fn)
): EventHandler<T> = once(T::class, handlerPosition, fn)
/**
* Suspends until an event of type [T] is emitted and returns it.
*/
suspend inline fun <reified T: E> waitFor(priority: EventPriority = EventPriority.NORMAL): T =
waitFor(T::class, priority)
suspend inline fun <reified T: E> waitFor(position: EventHandlerPosition = EventHandlerPosition.NORMAL): T =
waitFor(T::class, position)
}
abstract class EventBus: EventEmitter<Event>() {
/**
* Invokes all previously registered event handlers sorted by their priority.
* Invokes all previously registered event handlers sorted by their handlerPosition.
*
* The coroutine context is inherited.
*

View file

@ -6,7 +6,7 @@ import kotlin.reflect.KClass
class EventBusWrapper<E>(private val target: Any): EventEmitter<TargetedEvent<E>>() {
override fun <T : TargetedEvent<E>> on(
eventType: KClass<T>,
priority: EventPriority,
handlerPosition: EventHandlerPosition,
fn: suspend (event: T) -> Unit
): EventHandler<T> = Blokk.eventBus.on(eventType, priority) { event -> if (event.target == target) fn(event) }
): EventHandler<T> = Blokk.eventBus.on(eventType, handlerPosition) { event -> if (event.target == target) fn(event) }
}

View file

@ -1,13 +1,14 @@
package space.blokk.event
import space.blokk.plugin.Plugin
import java.util.*
import kotlin.reflect.KClass
data class EventHandler<T : Event> internal constructor(
private val eventEmitter: EventEmitter<*>,
val eventType: KClass<T>,
val plugin: Plugin?,
val priority: EventPriority,
val handlerPosition: EventHandlerPosition,
val fn: suspend (event: T) -> Unit
): Comparable<EventHandler<*>> {
/**
@ -15,5 +16,9 @@ data class EventHandler<T : Event> internal constructor(
*/
fun remove(): Boolean = eventEmitter.remove(this)
override fun compareTo(other: EventHandler<*>): Int = priority.compareTo(other.priority)
override fun compareTo(other: EventHandler<*>): Int {
val positionComparison = handlerPosition.compareTo(other.handlerPosition)
return if (positionComparison == 0) hashCode() - other.hashCode() else positionComparison
}
}

View file

@ -0,0 +1,12 @@
package space.blokk.event
import space.blokk.Blokk
open class EventHandlerPosition: Comparable<EventHandlerPosition> {
override fun compareTo(other: EventHandlerPosition): Int =
Blokk.eventHandlerPositions.positionOf(this) - Blokk.eventHandlerPositions.positionOf(other)
object FIRST: EventHandlerPosition()
object NORMAL: EventHandlerPosition()
object LAST: EventHandlerPosition()
}

View file

@ -0,0 +1,25 @@
package space.blokk.event
interface EventHandlerPositionManager {
fun positionOf(eventHandlerPosition: EventHandlerPosition): Int
fun positionOfOrNull(eventHandlerPosition: EventHandlerPosition): Int?
/**
* Shorthand for `insertBefore(EventHandlerPosition.LAST, new)`.
*/
fun insert(vararg positions: EventHandlerPosition)
/**
* Inserts [positions] before [existing].
*
* @throws IllegalArgumentException When [existing] is [EventHandlerPosition.FIRST]
*/
fun insertBefore(existing: EventHandlerPosition, vararg positions: EventHandlerPosition)
/**
* Inserts [positions] after [existing].
*
* @throws IllegalArgumentException When [existing] is [EventHandlerPosition.LAST]
*/
fun insertAfter(existing: EventHandlerPosition, vararg positions: EventHandlerPosition)
}

View file

@ -1,14 +0,0 @@
package space.blokk.event
// TODO: Develop a new event ordering concept
enum class EventPriority {
LOWEST,
LOW,
LOWER,
NORMAL,
HIGHER,
HIGH,
HIGHEST,
MONITOR,
INTERNAL
}

View file

@ -4,6 +4,7 @@ import space.blokk.Registry
import space.blokk.Scheduler
import space.blokk.command.Command
import space.blokk.event.EventBus
import space.blokk.event.EventHandlerPositionManager
import space.blokk.logging.Logger
import space.blokk.logging.LoggingOutputProvider
import space.blokk.net.Session
@ -17,6 +18,7 @@ import kotlin.coroutines.CoroutineContext
interface Server {
val eventBus: EventBus
val eventHandlerPositions: EventHandlerPositionManager
/**
* [CoroutineContext] confined to the server thread.

View file

@ -2,9 +2,6 @@ package space.blokk.world.block
import space.blokk.NamespacedID
/**
* Material: [DAYLIGHT_DETECTOR][Material.DAYLIGHT_DETECTOR]
*/
data class DaylightDetector(
@Attribute
val inverted: Boolean = false,
@ -12,9 +9,9 @@ data class DaylightDetector(
val power: Int
) : Block() {
companion object : Material<DaylightDetector> by material(
1,
333,
NamespacedID("minecraft:daylight_detector"),
6158,
6698,
0.2f,
true,
0,

View file

@ -6,12 +6,12 @@ import strikt.assertions.isEqualTo
class BlockCodecTest {
@Test
fun `getStateID returns 6158 for DaylightDetector(inverted=true, power=0)`() {
expectThat(DaylightDetector.codec.getStateID(DaylightDetector(true, 0))).isEqualTo(6158)
fun `getStateID returns 6698 for DaylightDetector(inverted=true, power=0)`() {
expectThat(DaylightDetector.codec.getStateID(DaylightDetector(true, 0))).isEqualTo(6698)
}
@Test
fun `getStateID returns 6189 for DaylightDetector(inverted=false, power=15)`() {
expectThat(DaylightDetector.codec.getStateID(DaylightDetector(false, 15))).isEqualTo(6189)
fun `getStateID returns 6729 for DaylightDetector(inverted=false, power=15)`() {
expectThat(DaylightDetector.codec.getStateID(DaylightDetector(false, 15))).isEqualTo(6729)
}
}

View file

@ -9,6 +9,7 @@ version = rootProject.version
repositories {
mavenCentral()
jcenter()
maven("https://jitpack.io")
}
@ -16,6 +17,8 @@ val coroutinesVersion = properties["version.kotlinx-coroutines"].toString()
val slf4jVersion = properties["version.slf4j"].toString()
val nettyVersion = properties["version.netty"].toString()
val moshiVersion = properties["version.moshi"].toString()
val junitVersion = properties["version.junit"].toString()
val striktVersion = properties["version.strikt"].toString()
dependencies {
// Kotlin
@ -46,6 +49,11 @@ dependencies {
implementation("com.sksamuel.hoplite:hoplite-core:1.3.9")
implementation("com.sksamuel.hoplite:hoplite-yaml:1.3.9")
kapt("com.squareup.moshi:moshi-kotlin-codegen:${moshiVersion}")
// Testing
testImplementation("io.strikt:strikt-core:${striktVersion}")
testImplementation("org.junit.jupiter:junit-jupiter-api:${junitVersion}")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:${junitVersion}")
}
tasks {
@ -57,6 +65,10 @@ tasks {
)
}
test {
useJUnitPlatform()
}
shadowJar {
archiveClassifier.set(null as String?)

View file

@ -39,6 +39,8 @@ class BlokkScheduler : Scheduler {
fun startTicking() {
val interval = 1000L / Server.TICKS_PER_SECOND
// TODO: Move executor initialization here
executor.scheduleAtFixedRate({
runBlocking {
val startTime = System.currentTimeMillis()

View file

@ -10,7 +10,9 @@ import kotlinx.coroutines.runBlocking
import space.blokk.command.Command
import space.blokk.config.BlokkConfig
import space.blokk.event.BlokkEventBus
import space.blokk.event.BlokkEventHandlerPositionManager
import space.blokk.event.EventBus
import space.blokk.event.EventHandlerPositionManager
import space.blokk.logging.BlokkLoggingOutputProvider
import space.blokk.logging.Logger
import space.blokk.net.BlokkSocketServer
@ -28,6 +30,8 @@ import java.util.concurrent.Executors
import kotlin.coroutines.CoroutineContext
import kotlin.system.exitProcess
// TODO: Consider using DI because this improves testability
class BlokkServer internal constructor() : Server {
val logger = Logger("Server")
private val socketServer = BlokkSocketServer(this)
@ -44,8 +48,6 @@ class BlokkServer internal constructor() : Server {
override val coroutineContext: CoroutineContext =
CoroutineName("Server") + Executors.newSingleThreadExecutor().asCoroutineDispatcher() + SupervisorJob()
override val eventBus = BlokkEventBus(this)
override val sessions by socketServer::sessions
override val players get() = sessions.mapNotNull { it.player }
@ -82,13 +84,8 @@ class BlokkServer internal constructor() : Server {
override val minimumLogLevel = config.minLogLevel
override val developmentMode: Boolean = config.developmentMode
init {
val clazz = Class.forName("space.blokk.BlokkKt")
val field = clazz.getDeclaredField("serverInstance")
field.isAccessible = true
field.set(null, this)
field.isAccessible = false
}
override val eventBus = BlokkEventBus(developmentMode)
override val eventHandlerPositions = BlokkEventHandlerPositionManager()
private fun failInitialization(t: Throwable): Nothing {
logger.error("Server initialization failed:", t)
@ -120,7 +117,15 @@ class BlokkServer internal constructor() : Server {
@JvmStatic
fun main(args: Array<String>) {
BlokkServer().start()
BlokkServer().also { setServerInstance(it) }.start()
}
fun setServerInstance(instance: Server) {
val clazz = Class.forName("space.blokk.BlokkKt")
val field = clazz.getDeclaredField("serverInstance")
field.isAccessible = true
field.set(null, instance)
field.isAccessible = false
}
}
}

View file

@ -10,18 +10,18 @@ import kotlin.coroutines.resume
import kotlin.reflect.KClass
import kotlin.system.measureTimeMillis
class BlokkEventBus(private val server: BlokkServer) : EventBus() {
class BlokkEventBus(private val developmentMode: Boolean) : EventBus() {
private val logger = Logger("EventBus", false)
/**
* Invokes all previously registered event handlers sorted by their priority.
* Invokes all previously registered event handlers sorted by their handlerPosition.
*
* The coroutine context is inherited.
*
* @return [event]
*/
override suspend fun <T : Event> emit(event: T): T {
if (server.developmentMode) {
if (developmentMode) {
var count = 0
val time = measureTimeMillis {
for (handler in handlers) {

View file

@ -0,0 +1,47 @@
package space.blokk.event
import java.lang.IllegalArgumentException
class BlokkEventHandlerPositionManager: EventHandlerPositionManager {
internal val positions = ArrayList<EventHandlerPosition>()
init {
positions.add(EventHandlerPosition.FIRST)
positions.add(EventHandlerPosition.NORMAL)
positions.add(EventHandlerPosition.LAST)
}
override fun positionOfOrNull(eventHandlerPosition: EventHandlerPosition): Int? =
positions.indexOf(eventHandlerPosition).let { if (it == -1) null else it }
override fun positionOf(eventHandlerPosition: EventHandlerPosition): Int = positionOfOrNull(eventHandlerPosition)
?: throw IllegalArgumentException("eventHandlerPosition was not registered")
override fun insert(vararg positions: EventHandlerPosition) {
insertBefore(EventHandlerPosition.LAST, *positions)
}
override fun insertBefore(existing: EventHandlerPosition, vararg positions: EventHandlerPosition) {
if (existing == EventHandlerPosition.FIRST)
throw IllegalArgumentException("You cannot insert positions before EventHandlerPosition.FIRST")
val index = positionOfOrNull(existing) ?: throw IllegalArgumentException("existing was not registered")
insertWithCheck(index, positions.distinct())
}
override fun insertAfter(existing: EventHandlerPosition, vararg positions: EventHandlerPosition) {
if (existing == EventHandlerPosition.LAST)
throw IllegalArgumentException("You cannot insert positions after EventHandlerPosition.LAST")
val index = positionOfOrNull(existing) ?: throw IllegalArgumentException("existing was not registered")
insertWithCheck(index + 1, positions.distinct())
}
private fun insertWithCheck(index: Int, positions: List<EventHandlerPosition>) {
for (position in positions) {
if (this.positions.indexOf(position) != -1) throw IllegalArgumentException("$position was already registered")
}
this.positions.addAll(index, positions)
}
}

View file

@ -4,21 +4,24 @@ import ch.qos.logback.classic.Level
import ch.qos.logback.classic.spi.ILoggingEvent
import ch.qos.logback.core.AppenderBase
import space.blokk.Blokk
import java.lang.Exception
class LogbackAppender : AppenderBase<ILoggingEvent>() {
override fun append(event: ILoggingEvent) {
Blokk.loggingOutputProvider.log(
true,
event.loggerName,
when (event.level) {
Level.TRACE -> Logger.Level.TRACE
Level.DEBUG -> Logger.Level.DEBUG
Level.INFO -> Logger.Level.INFO
Level.WARN -> Logger.Level.WARN
Level.ERROR -> Logger.Level.ERROR
else -> error("Should never happen")
},
event.message
)
try {
Blokk.loggingOutputProvider.log(
true,
event.loggerName,
when (event.level) {
Level.TRACE -> Logger.Level.TRACE
Level.DEBUG -> Logger.Level.DEBUG
Level.INFO -> Logger.Level.INFO
Level.WARN -> Logger.Level.WARN
Level.ERROR -> Logger.Level.ERROR
else -> error("Should never happen")
},
event.message
)
} catch(e: Throwable) {}
}
}

View file

@ -0,0 +1,104 @@
package space.blokk.event
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import space.blokk.Blokk
import space.blokk.BlokkServer
import strikt.api.expectThat
import strikt.api.expectThrows
import strikt.assertions.isEqualTo
import java.lang.IllegalArgumentException
class EventBusTest {
class TestEvent: Event() {
private var count = 0
fun test(expectedCount: Int) {
expectThat(count).isEqualTo(expectedCount)
count++
}
}
object Position1 : EventHandlerPosition()
object Position2 : EventHandlerPosition()
object Position3 : EventHandlerPosition()
object Position4 : EventHandlerPosition()
object Position5 : EventHandlerPosition()
object Position6 : EventHandlerPosition()
@BeforeEach
fun initServer() {
// Does not work because Blokk will not be reinitialized. When we use DI, we do no longer need this.
BlokkServer.setServerInstance(BlokkServer())
}
@Test
fun `handler positions cannot be inserted more than once`() {
Blokk.eventHandlerPositions.insert(Position1)
expectThrows<IllegalArgumentException> { Blokk.eventHandlerPositions.insert(Position1) }
expectThrows<IllegalArgumentException> { Blokk.eventHandlerPositions.insertAfter(EventHandlerPosition.FIRST, Position1) }
expectThrows<IllegalArgumentException> { Blokk.eventHandlerPositions.insertBefore(EventHandlerPosition.LAST, Position1) }
}
@Test
fun `handler positions are inserted where they should be`() {
val expectedOrder = listOf(
EventHandlerPosition.FIRST,
Position2,
Position4,
Position1,
Position5,
EventHandlerPosition.NORMAL,
Position3,
Position6,
EventHandlerPosition.LAST
)
val actualOrder = (Blokk.eventHandlerPositions as BlokkEventHandlerPositionManager).positions
println(actualOrder)
Blokk.eventHandlerPositions.insertAfter(EventHandlerPosition.FIRST, Position1, Position5)
println(actualOrder)
Blokk.eventHandlerPositions.insertBefore(Position1, Position2, Position4)
println(actualOrder)
Blokk.eventHandlerPositions.insert(Position3, Position6)
println(actualOrder)
actualOrder.forEachIndexed { index, position ->
expectThat(index).isEqualTo(expectedOrder.indexOf(position))
}
}
@Test
fun `handler positions cannot be inserted before the first or after the last`() {
expectThrows<IllegalArgumentException> {
Blokk.eventHandlerPositions.insertBefore(EventHandlerPosition.FIRST, Position1)
}
expectThrows<IllegalArgumentException> {
Blokk.eventHandlerPositions.insertAfter(EventHandlerPosition.LAST, Position1)
}
}
@Test
fun `handlers are invoked in the right order`() {
val order = listOf(
EventHandlerPosition.FIRST,
Position1,
EventHandlerPosition.NORMAL,
Position2,
Position3,
EventHandlerPosition.LAST
)
Blokk.eventHandlerPositions.insertAfter(EventHandlerPosition.FIRST, Position1)
Blokk.eventHandlerPositions.insertBefore(EventHandlerPosition.NORMAL, Position2)
Blokk.eventHandlerPositions.insert(Position3)
order.shuffled().forEach { pos -> Blokk.eventBus.on<TestEvent>(pos) { it.test(order.indexOf(pos)) } }
runBlocking { Blokk.eventBus.emit(TestEvent()) }
}
}