aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorbush-did-711 <39170869+bush-did-711@users.noreply.github.com>2022-03-30 23:03:09 -1000
committerbush-did-711 <39170869+bush-did-711@users.noreply.github.com>2022-03-30 23:03:09 -1000
commit0f5fdefed68d4d54f08d8e9fc9dc3a0e098f93a1 (patch)
tree5f773a9927046a0a0c41c4fb01bcda48dc3cbf6b
parent1246f0f644eb1fca30f92c601941937506ca2beb (diff)
downloadeventbus-kotlin-0f5fdefed68d4d54f08d8e9fc9dc3a0e098f93a1.tar.gz
eventbus-kotlin-0f5fdefed68d4d54f08d8e9fc9dc3a0e098f93a1.tar.bz2
eventbus-kotlin-0f5fdefed68d4d54f08d8e9fc9dc3a0e098f93a1.zip
started adding tests, more refactoring (not done)
-rw-r--r--build.gradle.kts8
-rw-r--r--src/main/kotlin/me/bush/illnamethislater/CancelState.kt46
-rw-r--r--src/main/kotlin/me/bush/illnamethislater/Event.kt11
-rw-r--r--src/main/kotlin/me/bush/illnamethislater/EventBus.kt19
-rw-r--r--src/main/kotlin/me/bush/illnamethislater/Listener.kt17
-rw-r--r--src/main/kotlin/me/bush/illnamethislater/ListenerGroup.kt4
-rw-r--r--src/main/kotlin/me/bush/illnamethislater/ReflectUtil.kt57
-rw-r--r--src/main/kotlin/me/bush/illnamethislater/Util.kt83
-rw-r--r--src/test/kotlin/Main.kt66
-rw-r--r--src/test/kotlin/Test.kt151
10 files changed, 288 insertions, 174 deletions
diff --git a/build.gradle.kts b/build.gradle.kts
index a1450bd..e253db8 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,4 +1,7 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+import org.gradle.api.tasks.testing.logging.TestLogEvent.*
+import org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
+import kotlin.collections.setOf
plugins {
kotlin("jvm") version "1.6.10"
@@ -14,7 +17,6 @@ repositories {
dependencies {
testImplementation(kotlin("test"))
- // Staying on the version used by 1.12.2 to avoid possible incompatibilities.
// 2.15.0 is not vulnerable to any RCE exploits.
implementation("org.apache.logging.log4j:log4j-api:2.15.0")
implementation("org.apache.logging.log4j:log4j-core:2.15.0")
@@ -25,6 +27,10 @@ dependencies {
}
tasks.test {
+ testLogging {
+ exceptionFormat = FULL
+ showStandardStreams = true
+ }
useJUnitPlatform()
}
diff --git a/src/main/kotlin/me/bush/illnamethislater/CancelState.kt b/src/main/kotlin/me/bush/illnamethislater/CancelState.kt
new file mode 100644
index 0000000..270d88f
--- /dev/null
+++ b/src/main/kotlin/me/bush/illnamethislater/CancelState.kt
@@ -0,0 +1,46 @@
+package me.bush.illnamethislater
+
+import sun.misc.Unsafe
+import kotlin.reflect.KClass
+import kotlin.reflect.KMutableProperty1
+import kotlin.reflect.full.declaredMembers
+import kotlin.reflect.full.isSubclassOf
+import kotlin.reflect.jvm.javaField
+import kotlin.reflect.typeOf
+
+/**
+ * @author bush
+ * @since 1.0.0
+ */
+internal fun interface CancelledState {
+
+ /**
+ * Will either return false, cast [event] to [Event] and return [Event.cancelled], or use
+ * [Unsafe.getBoolean] to get the value of the "cancelled" field of an external event.
+ *
+ * @author bush
+ * @since 1.0.0
+ */
+ fun isCancelled(event: Any): Boolean
+
+ companion object {
+ private val UNSAFE = Unsafe::class.declaredMembers.single { it.name == "theUnsafe" }.handleCall() as Unsafe
+
+ // This is really just for interoperability with forge events, but ig it would work with anything
+ private val CANCELLED_NAMES = arrayOf("canceled", "cancelled")
+ private val NOT_CANCELLABLE by lazy { CancelledState { false } }
+ private val OFFSETS = hashMapOf<KClass<*>, Long>()
+
+ fun cancelStateOf(type: KClass<*>, bus: EventBus): CancelledState {
+ if (type.isSubclassOf(Event::class)) return CancelledState { (it as Event).cancelled }
+ if (!bus.externalSupport) return NOT_CANCELLABLE
+ type.allMembers.filter { it.name in CANCELLED_NAMES && it.returnType == typeOf<Boolean>() }
+ .filterIsInstance<KMutableProperty1<*, *>>().toList().let {
+ if (it.isEmpty()) return NOT_CANCELLABLE
+ if (it.size != 1) bus.logger.warn("Multiple possible cancel fields found for event type $type")
+ OFFSETS[type] = UNSAFE.objectFieldOffset(it[0].javaField)
+ return CancelledState { UNSAFE.getBoolean(it, OFFSETS[type]!!) }
+ }
+ }
+ }
+}
diff --git a/src/main/kotlin/me/bush/illnamethislater/Event.kt b/src/main/kotlin/me/bush/illnamethislater/Event.kt
index 39360eb..dedd4df 100644
--- a/src/main/kotlin/me/bush/illnamethislater/Event.kt
+++ b/src/main/kotlin/me/bush/illnamethislater/Event.kt
@@ -1,19 +1,16 @@
package me.bush.illnamethislater
/**
- * A base class for events that can be cancelled.
+ * A base class for events that can be cancelled. More information can be found
+ * [here](https://github.com/therealbush/eventbus-kotlin)
*
- * If [cancellable] is true, your event can be [cancelled].
- *
- * If [cancelled] is true, listeners with lower priority will not receive it unless:
+ * If [cancellable] is `true`, your event can be [cancelled], where future listeners will not receive it unless:
* * They have [Listener.receiveCancelled] set to `true`.
- * * A future listener with [Listener.receiveCancelled] sets [cancelled] to `false`
+ * * A previous listener with [Listener.receiveCancelled] sets [cancelled] back to `false`
*
* @author bush
* @since 1.0.0
*/
-// TODO: 3/27/2022 ducks or a way to cancel anything (can't put a custom annotation on forge events)
-// store cancellable info at subscribe time, do not calculate it in post
abstract class Event {
var cancelled = false
set(value) {
diff --git a/src/main/kotlin/me/bush/illnamethislater/EventBus.kt b/src/main/kotlin/me/bush/illnamethislater/EventBus.kt
index 66424f7..01d90cd 100644
--- a/src/main/kotlin/me/bush/illnamethislater/EventBus.kt
+++ b/src/main/kotlin/me/bush/illnamethislater/EventBus.kt
@@ -4,18 +4,23 @@ import org.apache.logging.log4j.LogManager
import org.apache.logging.log4j.Logger
import kotlin.reflect.KClass
+// TODO: 3/30/2022 Refactor some stuff
+
/**
* [A simple event dispatcher](http://github.com/therealbush/eventbus-kotlin)
*
* @author bush
* @since 1.0.0
*/
-class EventBus(private val logger: Logger = LogManager.getLogger()) {
+class EventBus( // todo encapsulation??
+ internal val logger: Logger = LogManager.getLogger("EVENTBUS"),
+ internal val externalSupport: Boolean = true
+) {
private val listeners = hashMapOf<KClass<*>, ListenerGroup>()
private val subscribers = mutableSetOf<Any>()
/**
- * blah blah annotation properties override listener properties
+ * doc
*/
infix fun subscribe(subscriber: Any): Boolean {
return if (subscriber in subscribers) false
@@ -38,15 +43,15 @@ class EventBus(private val logger: Logger = LogManager.getLogger()) {
*/
fun register(listener: Listener, subscriber: Any = Any()): Any {
listener.subscriber = subscriber
- listeners.computeIfAbsent(listener.type, ::ListenerGroup).addListener(listener)
+ listeners.computeIfAbsent(listener.type) { ListenerGroup(CancelledState.cancelStateOf(listener.type, this)) }
+ .addListener(listener)
subscribers += subscriber
return subscriber
}
/**
- *
+ * doc
*/
- // doc
infix fun unsubscribe(subscriber: Any) = subscribers.remove(subscriber).apply {
if (this) listeners.entries.removeIf {
it.value.removeFrom(subscriber)
@@ -55,10 +60,10 @@ class EventBus(private val logger: Logger = LogManager.getLogger()) {
}
/**
- * Posts an event.
+ * Posts an event. doc
*/
- // doc
infix fun post(event: Any) = listeners[event::class]?.let { group ->
+ // TODO: 3/30/2022 rewrite this all lol priority isn't even set up yet
group.sequential.forEach {
if (!group.cancelState.isCancelled(event) || it.receiveCancelled) {
it.listener(event)
diff --git a/src/main/kotlin/me/bush/illnamethislater/Listener.kt b/src/main/kotlin/me/bush/illnamethislater/Listener.kt
index 9e7e25e..f7a2a81 100644
--- a/src/main/kotlin/me/bush/illnamethislater/Listener.kt
+++ b/src/main/kotlin/me/bush/illnamethislater/Listener.kt
@@ -3,32 +3,35 @@ package me.bush.illnamethislater
import kotlin.reflect.KClass
/**
- * This class is not intended to be used externally, use [listener] instead.
- *
- * You *could* use this, and it would work fine, however
- * [listener] lets us use reified types for a cleaner look.
+ * This class is not intended to be used externally, use [listener] instead. You *could* use this,
+ * and it would work fine however you would have to specify the type explicitly. (ew!)
*
* @author bush
* @since 1.0.0
*/
class Listener @PublishedApi internal constructor(
listener: (Nothing) -> Unit,
+ internal val type: KClass<*>,
internal var priority: Int = 0,
internal var parallel: Boolean = false,
- internal var receiveCancelled: Boolean = false,
- internal val type: KClass<*>
+ internal var receiveCancelled: Boolean = false
) {
@Suppress("UNCHECKED_CAST")
+ // Generics have no benefit here,
+ // it is easier just to force cast.
internal val listener = listener as (Any) -> Unit
internal lateinit var subscriber: Any
}
/**
+ * doc later
*
+ * @author bush
+ * @since 1.0.0
*/
inline fun <reified T : Any> listener(
priority: Int = 0,
parallel: Boolean = false,
receiveCancelled: Boolean = false,
noinline listener: (T) -> Unit
-) = Listener(listener, priority, parallel, receiveCancelled, T::class)
+) = Listener(listener, T::class, priority, parallel, receiveCancelled)
diff --git a/src/main/kotlin/me/bush/illnamethislater/ListenerGroup.kt b/src/main/kotlin/me/bush/illnamethislater/ListenerGroup.kt
index d8003a4..cef2a0c 100644
--- a/src/main/kotlin/me/bush/illnamethislater/ListenerGroup.kt
+++ b/src/main/kotlin/me/bush/illnamethislater/ListenerGroup.kt
@@ -1,16 +1,14 @@
package me.bush.illnamethislater
import java.util.concurrent.CopyOnWriteArrayList
-import kotlin.reflect.KClass
/**
* @author bush
* @since 1.0.0
*/
-internal class ListenerGroup(type: KClass<*>) {
+internal class ListenerGroup(val cancelState: CancelledState) {
val parallel = CopyOnWriteArrayList<Listener>()
val sequential = CopyOnWriteArrayList<Listener>()
- val cancelState = cancelStateOf(type)
val isEmpty get() = parallel.isEmpty() && sequential.isEmpty()
diff --git a/src/main/kotlin/me/bush/illnamethislater/ReflectUtil.kt b/src/main/kotlin/me/bush/illnamethislater/ReflectUtil.kt
new file mode 100644
index 0000000..d03fe58
--- /dev/null
+++ b/src/main/kotlin/me/bush/illnamethislater/ReflectUtil.kt
@@ -0,0 +1,57 @@
+package me.bush.illnamethislater
+
+import java.lang.reflect.Modifier
+import kotlin.reflect.KCallable
+import kotlin.reflect.KClass
+import kotlin.reflect.KProperty
+import kotlin.reflect.full.allSuperclasses
+import kotlin.reflect.full.declaredMembers
+import kotlin.reflect.jvm.isAccessible
+import kotlin.reflect.jvm.javaField
+import kotlin.reflect.jvm.javaGetter
+import kotlin.reflect.typeOf
+
+/**
+ * Using [KClass.members] only returns public members, and using [KClass.declaredMembers]
+ * doesn't return inherited members. This returns all members, private and inherited.
+ *
+ * @author bush
+ * @since 1.0.0
+ */
+internal val <T : Any> KClass<T>.allMembers
+ get() = (declaredMembers + allSuperclasses.flatMap { it.declaredMembers }).asSequence()
+
+/**
+ * Checks if a [KCallable] is static on the jvm, and handles invocation accordingly.
+ * I am not aware of a better alternative that works with `object` classes.
+ *
+ * @author bush
+ * @since 1.0.0
+ */
+internal fun <R> KCallable<R>.handleCall(receiver: Any? = null): R {
+ isAccessible = true
+ return if (static) call() else call(receiver)
+}
+
+/**
+ * Checks if the calling [KCallable] is a static java field. Because Kotlin likes to be funny, properties
+ * belonging to `object` classes are static, but their getters are not. If there is a getter (the property
+ * is not private), we will be accessing that, otherwise we check if the field is static with Java reflection.
+ * This also lets us support static listeners in Java code.
+ *
+ * @author bush
+ * @since 1.0.0
+ */
+internal val KCallable<*>.static
+ get() = if (this !is KProperty<*> || javaGetter != null) false
+ else javaField?.let { Modifier.isStatic(it.modifiers) } ?: false
+
+/**
+ * Finds all listeners in a class. (properties and methods)
+ *
+ * @author bush
+ * @since 1.0.0
+ */
+@Suppress("UNCHECKED_CAST") // This cannot fail
+internal inline val KClass<*>.listeners
+ get() = allMembers.filter { it.returnType == typeOf<Listener>() } as Sequence<KCallable<Listener>>
diff --git a/src/main/kotlin/me/bush/illnamethislater/Util.kt b/src/main/kotlin/me/bush/illnamethislater/Util.kt
deleted file mode 100644
index ab486ab..0000000
--- a/src/main/kotlin/me/bush/illnamethislater/Util.kt
+++ /dev/null
@@ -1,83 +0,0 @@
-package me.bush.illnamethislater
-
-import sun.misc.Unsafe
-import java.lang.reflect.Modifier
-import kotlin.reflect.*
-import kotlin.reflect.full.allSuperclasses
-import kotlin.reflect.full.declaredMembers
-import kotlin.reflect.full.isSubclassOf
-import kotlin.reflect.full.isSubtypeOf
-import kotlin.reflect.jvm.javaField
-import kotlin.reflect.jvm.javaGetter
-
-// author bush
-// since 1.0.0
-
-/**
- * Using [KClass.members] only returns public members, and
- * using [KClass.declaredMembers] doesn't return inherited
- * members. This returns all members, private and inherited.
- */
-internal val <T : Any> KClass<T>.allMembers
- get() = (declaredMembers + allSuperclasses.flatMap { it.declaredMembers }).asSequence()
-
-/**
- * Checks if a [KCallable] is static on the jvm, and handles invocation accordingly.
- *
- * I am not aware of a better alternative that works with `object` classes.
- */
-internal fun <R> KCallable<R>.handleCall(receiver: Any) = if (static) call() else call(receiver)
-
-/**
- * Checks if the calling [KCallable] is a static java field.
- *
- * Because kotlin likes to be funny, properties belonging to
- * `object` classes are static, but their getters are not.
- *
- * If there is a getter (the property is not private),
- * we will be accessing it through that, so we can stop checking.
- *
- * Otherwise, we check if the field is static with java reflection.
- */
-internal val KCallable<*>.static
- get() = if (this !is KProperty<*> || javaGetter != null) false
- else javaField?.let { Modifier.isStatic(it.modifiers) } ?: false
-
-/**
- * Finds all listeners in a class. (properties and methods)
- */
-@Suppress("UNCHECKED_CAST") // This cannot fail
-internal inline val KClass<*>.listeners
- get() = allMembers.filter { it.returnType == typeOf<Listener>() } as Sequence<KCallable<Listener>>
-
-internal fun interface CancelledState {
- fun isCancelled(event: Any): Boolean
-}
-
-private val unsafe = Unsafe::class.java.getDeclaredField("theUnsafe").let {
- it.isAccessible = true
- it.get(null) as Unsafe
-}
-
-private val offsetMap = hashMapOf<KClass<*>, Long>()
-
-private val possibleNames = arrayOf("canceled", "cancelled")
-
-private val NOT_CANCELLABLE = CancelledState { false }
-
-internal fun cancelStateOf(type: KClass<*>) = when {
- type.isSubclassOf(Event::class) -> CancelledState { (it as Event).cancelled }
- else -> findCancelField(type)?.let {
- offsetMap[type] = unsafe.objectFieldOffset(it.javaField)
- CancelledState { event -> unsafe.getBoolean(event, offsetMap[type]!!) }
- } ?: NOT_CANCELLABLE
-}
-
-private fun findCancelField(type: KClass<*>) = type.allMembers
- .filter { it.name in possibleNames && it.returnType == typeOf<Boolean>() }
- .filterIsInstance<KMutableProperty1<*, *>>().toList().let {
- if (it.isEmpty()) null else {
- if (it.size != 1) TODO() // warn
- it[0]
- }
- }
diff --git a/src/test/kotlin/Main.kt b/src/test/kotlin/Main.kt
deleted file mode 100644
index 7000c2d..0000000
--- a/src/test/kotlin/Main.kt
+++ /dev/null
@@ -1,66 +0,0 @@
-import me.bush.illnamethislater.*
-import org.apache.logging.log4j.Level
-import org.apache.logging.log4j.LogManager
-import org.apache.logging.log4j.core.config.Configurator
-
-/**
- * @author bush
- * @since 1.0.0
- */
-fun main() {
- Configurator.setRootLevel(Level.INFO)
-
- EventBus().run {
-
- subscribe(Subscriber())
-
- post(External())
-
-// val key = register(listener<Int> {
-// println(it)
-// })
-//
-// val topLevelListenerKey = register(topLevelListener())
-//
-// unsubscribe(key)
-//
-// unsubscribe(topLevelListenerKey)
-//
-// debugInfo()
- }
-
-}
-
-fun topLevelListener() = listener<Int> {
- println("topLevelListener(): $it")
-}
-
-class Subscriber {
-
- val listener0 = listener<External>(500) {
- println("listener 0")
- println(it.canceled)
- it.canceled = true
- }
-
- val listener1 = listener<External>(250, receiveCancelled = true) {
- println("listener 1")
- println(it.canceled)
- it.canceled = false
- }
-
- val listener2 get() = listener<External> {
- println("listener 2")
- println(it.canceled)
- it.canceled = true
- }
-
- fun listener3() = listener<External>(-250) {
- println("listener 3")
- println(it.canceled)
- }
-}
-
-class External {
- var canceled = false
-}
diff --git a/src/test/kotlin/Test.kt b/src/test/kotlin/Test.kt
new file mode 100644
index 0000000..e7fb4cc
--- /dev/null
+++ b/src/test/kotlin/Test.kt
@@ -0,0 +1,151 @@
+import me.bush.illnamethislater.Event
+import me.bush.illnamethislater.EventBus
+import me.bush.illnamethislater.listener
+import org.apache.logging.log4j.Level
+import org.apache.logging.log4j.LogManager
+import org.apache.logging.log4j.core.config.Configurator
+import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.api.BeforeAll
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.TestInstance
+import org.junit.jupiter.api.TestInstance.Lifecycle
+import kotlin.random.Random
+
+/**
+ * I don't know how to do these....
+ *
+ * @author bush
+ * @since 1.0.0
+ */
+@TestInstance(Lifecycle.PER_CLASS)
+class Test {
+ lateinit var eventBus: EventBus
+ private val logger = LogManager.getLogger()
+
+ @BeforeAll
+ fun setup() {
+ Configurator.setRootLevel(Level.ALL)
+
+ logger.info("Initializing")
+ eventBus = EventBus()
+
+ logger.info("Logging empty debug info")
+ eventBus.debugInfo()
+
+ logger.info("Subscribing")
+ eventBus.subscribe(this)
+
+ logger.info("Testing Events")
+ }
+
+ // Tests autoboxing and internal data structure against primitives.
+ @Test
+ fun primitiveListenerTest() {
+ val random = Random.nextInt()
+ eventBus.post(random)
+ Assertions.assertEquals(random, primitiveTestValue)
+ }
+
+ var primitiveTestValue = 0
+
+ val primitiveListener = listener<Int> {
+ primitiveTestValue = it
+ }
+
+ // Tests unsubscribing of "free" listeners which don't belong to a subscriber. todo allow keys to be resubscribed and test top level listeners
+ @Test
+ fun freeListenerTest() {
+ // Register "free" listener, and keep the returned key
+ val key = eventBus.register(listener<String> {
+ freeListenerTestValue = it
+ })
+ val valueOne = "i love bush's eventbus <3"
+ val valueTwo = "sdklasdjsakdsadlksadlksdl"
+ // Will change the value
+ eventBus.post(valueOne)
+ Assertions.assertEquals(valueOne, freeListenerTestValue)
+ // Remove the listener
+ eventBus.unsubscribe(key)
+ // No effect
+ eventBus.post(valueTwo)
+ // Value will not change
+ Assertions.assertEquals(valueOne, freeListenerTestValue)
+ }
+
+ var freeListenerTestValue: String? = null
+
+ // Tests priority and receiveCancelled functionality.
+ @Test
+ fun myEventListenerTest() {
+ val event = MyEvent()
+ eventBus.post(event)
+ Assertions.assertEquals(event.lastListener, "myEventListener3")
+ }
+
+ // First to be called; highest priority.
+ val myEventListener0 = listener<MyEvent>(priority = 10) {
+ Assertions.assertEquals(it.lastListener, "")
+ it.lastListener = "myEventListener0"
+ it.cancel()
+ }
+
+ // Will not be called; second-highest priority, no receiveCancelled.
+ val myEventListener1
+ get() = listener<MyEvent>(priority = 0) {
+ Assertions.assertTrue(false)
+ }
+
+ // Second to be called; has receiveCancelled and can un-cancel the event.
+ fun myEventListener2() = listener<MyEvent>(priority = Int.MIN_VALUE + 100, receiveCancelled = true) {
+ Assertions.assertEquals(it.lastListener, "myEventListener0")
+ it.lastListener = "myEventListener2"
+ it.cancelled = false
+ }
+
+ // Last to be called; does not have receiveCancelled, but the last listener un-cancelled the event.
+ fun myEventListener3() = listener<MyEvent>(priority = Int.MIN_VALUE) {
+ Assertions.assertEquals(it.lastListener, "myEventListener2")
+ it.lastListener = "myEventListener3"
+ }
+
+ // Tests external event cancel state functionality.
+ @Test
+ fun externalEventListenerTest() {
+
+ }
+
+ // Tests parallel invocation functionality. todo how will this work with cancellability
+ @Test
+ fun parallelListenerTest() {
+
+ }
+
+ // Tests reflection against singleton object classes.
+ @Test
+ fun objectSubscriberTest() {
+
+ }
+
+ // todo what else?
+}
+
+// todo test changing cancellability
+class MyEvent : Event() {
+ override val cancellable = true
+
+ var lastListener = ""
+}
+
+class ExternalEvent0 {
+ var canceled = false
+}
+
+class ExternalEvent1 {
+ var cancelled = false
+}
+
+// Should give us a warning about duplicates
+class ExternalEvent2 {
+ var canceled = false
+ var cancelled = false
+}