diff --git a/src/main/kotlin/no/iktdev/eventi/events/EventDispatcher.kt b/src/main/kotlin/no/iktdev/eventi/events/EventDispatcher.kt index 06c6539..b20f4aa 100644 --- a/src/main/kotlin/no/iktdev/eventi/events/EventDispatcher.kt +++ b/src/main/kotlin/no/iktdev/eventi/events/EventDispatcher.kt @@ -14,9 +14,14 @@ open class EventDispatcher(val eventStore: EventStore) { .filter { it.eventId !in derivedFromIds } .filter { it.eventId !in deletedEventIds } + val effectiveHistory = events + .filter { it.eventId !in deletedEventIds } // fjern slettede events + .filterNot { it is DeleteEvent } // fjern selve delete-eventet + + EventListenerRegistry.getListeners().forEach { listener -> for (candidate in candidates) { - val result = listener.onEvent(candidate, events) + val result = listener.onEvent(candidate, effectiveHistory) if (result != null) { eventStore.persist(result) } diff --git a/src/test/kotlin/no/iktdev/eventi/EventDispatcherTest.kt b/src/test/kotlin/no/iktdev/eventi/EventDispatcherTest.kt index 9d32c07..8464cfe 100644 --- a/src/test/kotlin/no/iktdev/eventi/EventDispatcherTest.kt +++ b/src/test/kotlin/no/iktdev/eventi/EventDispatcherTest.kt @@ -20,37 +20,47 @@ import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import java.util.UUID -class EventDispatcherTest: TestBase() { +@DisplayName(""" +EventDispatcher +Når hendelser dispatches til lyttere +Hvis hendelsene inneholder avledede, slettede eller nye events +Så skal dispatcheren håndtere filtrering, replays og historikk korrekt +""") +class EventDispatcherTest : TestBase() { + val dispatcher = EventDispatcher(eventStore) - class DerivedEvent(): Event() - class TriggerEvent(): Event() { - } - class OtherEvent(): Event() - class DummyEvent(): Event() { + class DerivedEvent : Event() + class TriggerEvent : Event() + class OtherEvent : Event() + class DummyEvent : Event() { fun putMetadata(metadata: Metadata) { this.metadata = metadata } } - @BeforeEach fun setup() { EventTypeRegistry.wipe() EventListenerRegistry.wipe() - // Verifiser at det er tomt - EventTypeRegistry.register(listOf( - DerivedEvent::class.java, - TriggerEvent::class.java, - OtherEvent::class.java, - DummyEvent::class.java - )) + EventTypeRegistry.register( + listOf( + DerivedEvent::class.java, + TriggerEvent::class.java, + OtherEvent::class.java, + DummyEvent::class.java + ) + ) } - @Test - fun `should produce one event and stop`() { + @DisplayName(""" + Når en TriggerEvent dispatches + Hvis en lytter produserer én DerivedEvent + Så skal kun én ny event produseres og prosessen stoppe + """) + fun shouldProduceOneEventAndStop() { val listener = ProducingListener() val trigger = TriggerEvent() @@ -66,7 +76,12 @@ class EventDispatcherTest: TestBase() { } @Test - fun `should skip already derived events`() { + @DisplayName(""" + Når en event allerede har avledet en DerivedEvent + Hvis dispatcheren replays historikken + Så skal ikke DerivedEvent produseres på nytt + """) + fun shouldSkipAlreadyDerivedEvents() { val listener = ProducingListener() val trigger = TriggerEvent() @@ -76,11 +91,16 @@ class EventDispatcherTest: TestBase() { dispatcher.dispatch(trigger.referenceId, listOf(trigger, derived!!.toEvent()!!)) - assertEquals(1, eventStore.all().size) // no new event produced + assertEquals(1, eventStore.all().size) } @Test - fun `should pass full context to listener`() { + @DisplayName(""" + Når flere events dispatches + Hvis en lytter mottar en event + Så skal hele historikken leveres i context + """) + fun shouldPassFullContextToListener() { val listener = ContextCapturingListener() val e1 = TriggerEvent() @@ -91,7 +111,12 @@ class EventDispatcherTest: TestBase() { } @Test - fun `should behave deterministically across replays`() { + @DisplayName(""" + Når en replay skjer + Hvis en event allerede har produsert en DerivedEvent + Så skal ikke DerivedEvent produseres på nytt + """) + fun shouldBehaveDeterministicallyAcrossReplays() { val listener = ProducingListener() val trigger = TriggerEvent() @@ -100,13 +125,19 @@ class EventDispatcherTest: TestBase() { dispatcher.dispatch(trigger.referenceId, replayContext) - assertEquals(1, eventStore.all().size) // no duplicate + assertEquals(1, eventStore.all().size) } @Test - fun `should not deliver deleted events as candidates`() { + @DisplayName(""" + Når en DeleteEvent peker på en tidligere event + Hvis dispatcheren filtrerer kandidater + Så skal slettede events ikke leveres som kandidater + """) + fun shouldNotDeliverDeletedEventsAsCandidates() { val dispatcher = EventDispatcher(eventStore) val received = mutableListOf() + object : EventListener() { override fun onEvent(event: Event, history: List): Event? { received += event @@ -135,7 +166,12 @@ class EventDispatcherTest: TestBase() { } @Test - fun `should deliver DeleteEvent to listeners that react to it`() { + @DisplayName(""" + Når en DeleteEvent dispatches alene + Hvis en lytter reagerer på DeleteEvent + Så skal DeleteEvent leveres som kandidat + """) + fun shouldDeliverDeleteEventToListenersThatReactToIt() { val received = mutableListOf() val listener = object : EventListener() { override fun onEvent(event: Event, context: List): Event? { @@ -144,16 +180,19 @@ class EventDispatcherTest: TestBase() { } } - val deleted = object : DeleteEvent(UUID.randomUUID()) { - } + val deleted = object : DeleteEvent(UUID.randomUUID()) {} dispatcher.dispatch(deleted.referenceId, listOf(deleted)) assertTrue(received.contains(deleted)) } @Test - @DisplayName("Replay skal ikke levere en event som allerede har avledet en ny") - fun `should not re-deliver events that have produced derived events`() { + @DisplayName(""" + Når en event har avledet en ny event + Hvis dispatcheren replays historikken + Så skal ikke original-eventen leveres som kandidat igjen + """) + fun shouldNotRedeliverEventsThatHaveProducedDerivedEvents() { val listener = ProducingListener() val trigger = TriggerEvent() @@ -175,7 +214,90 @@ class EventDispatcherTest: TestBase() { } } + @Test + @DisplayName(""" + Når en DeleteEvent slettet en tidligere event + Hvis dispatcheren bygger historikk + Så skal slettede events ikke være med i history + """) + fun historyShouldExcludeDeletedEvents() { + val dispatcher = EventDispatcher(eventStore) + val original = TriggerEvent() + val deleted = object : DeleteEvent(original.eventId) {} + + var receivedHistory: List = emptyList() + + val listener = object : EventListener() { + override fun onEvent(event: Event, history: List): Event? { + receivedHistory = history + return null + } + } + + dispatcher.dispatch(original.referenceId, listOf(original, deleted)) + + assertFalse(receivedHistory.contains(original)) + assertFalse(receivedHistory.contains(deleted)) + } + + @Test + @DisplayName(""" + Når en DeleteEvent slettet en event + Hvis andre events fortsatt er gyldige + Så skal history kun inneholde de ikke-slettede events + """) + fun historyShouldKeepNonDeletedEvents() { + val dispatcher = EventDispatcher(eventStore) + + val e1 = TriggerEvent() + val e2 = OtherEvent() + val deleted = object : DeleteEvent(e1.eventId) {} + + var receivedHistory: List = emptyList() + + val listener = object : EventListener() { + override fun onEvent(event: Event, history: List): Event? { + receivedHistory = history + return null + } + } + + dispatcher.dispatch(e1.referenceId, listOf(e1, e2, deleted)) + + assertTrue(receivedHistory.contains(e2)) + assertFalse(receivedHistory.contains(e1)) + assertFalse(receivedHistory.contains(deleted)) + } + + @Test + @DisplayName(""" + Når en DeleteEvent er kandidat + Hvis historikken kun inneholder slettede events + Så skal history være tom + """) + fun deleteEventShouldBeDeliveredButHistoryEmpty() { + val dispatcher = EventDispatcher(eventStore) + + val original = TriggerEvent() + val deleted = object : DeleteEvent(original.eventId) {} + + var receivedEvent: Event? = null + var receivedHistory: List = emptyList() + + val listener = object : EventListener() { + override fun onEvent(event: Event, history: List): Event? { + receivedEvent = event + receivedHistory = history + return null + } + } + + dispatcher.dispatch(original.referenceId, listOf(original, deleted)) + + assertTrue(receivedEvent is DeleteEvent) + assertTrue(receivedHistory.isEmpty()) + } // --- Test helpers --- diff --git a/src/test/kotlin/no/iktdev/eventi/ZDSTest.kt b/src/test/kotlin/no/iktdev/eventi/ZDSTest.kt index 113ce6e..689a07b 100644 --- a/src/test/kotlin/no/iktdev/eventi/ZDSTest.kt +++ b/src/test/kotlin/no/iktdev/eventi/ZDSTest.kt @@ -13,18 +13,29 @@ import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test +@DisplayName(""" +ZDS – Serialization/Deserialization System +Når Event- og Task-objekter persisteres og gjenopprettes +Hvis type-registrene er korrekt konfigurert +Så skal ZDS kunne serialisere og deserialisere objektene uten tap av data +""") class ZDSTest { @BeforeEach fun setup() { EventTypeRegistry.wipe() TaskTypeRegistry.wipe() + // Verifiser at det er tomt assertNull(EventTypeRegistry.resolve("SomeEvent")) } @Test - @DisplayName("Test ZDS with Event object") + @DisplayName(""" + Når et Event-objekt persisteres via ZDS + Hvis typen er registrert i EventTypeRegistry + Så skal det kunne gjenopprettes som riktig Event-type med samme data + """) fun scenario1() { EventTypeRegistry.register(EchoEvent::class.java) @@ -34,17 +45,19 @@ class ZDSTest { val restored = persisted!!.toEvent() assert(restored is EchoEvent) assert((restored as EchoEvent).data == "hello") - } data class TestTask( val data: String? - ): Task() + ) : Task() @Test - @DisplayName("Test ZDS with Task object") + @DisplayName(""" + Når et Task-objekt persisteres via ZDS + Hvis typen er registrert i TaskTypeRegistry + Så skal det kunne gjenopprettes som riktig Task-type med metadata intakt + """) fun scenario2() { - TaskTypeRegistry.register(TestTask::class.java) val task = TestTask("Potato") @@ -57,7 +70,5 @@ class ZDSTest { assert((restored as TestTask).data == "Potato") assert(restored.metadata.created == task.metadata.created) assert(restored.metadata.derivedFromId == task.metadata.derivedFromId) - } - -} \ No newline at end of file +} diff --git a/src/test/kotlin/no/iktdev/eventi/events/EventListenerRegistryTest.kt b/src/test/kotlin/no/iktdev/eventi/events/EventListenerRegistryTest.kt index b166910..1ccacd5 100644 --- a/src/test/kotlin/no/iktdev/eventi/events/EventListenerRegistryTest.kt +++ b/src/test/kotlin/no/iktdev/eventi/events/EventListenerRegistryTest.kt @@ -6,35 +6,34 @@ import no.iktdev.eventi.testUtil.wipe import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test +@DisplayName(""" +EventListenerRegistry +Når lyttere registreres med og uten @ListenerOrder +Hvis registry sorterer dem etter annotasjonen +Så skal rekkefølgen være deterministisk og korrekt +""") class EventListenerRegistryTest { @ListenerOrder(1) - class MockTest1() : EventListener() { - override fun onEvent(event: Event, history: List): Event? { - return null - } + class MockTest1 : EventListener() { + override fun onEvent(event: Event, history: List): Event? = null } @ListenerOrder(2) - class MockTest2() : EventListener() { - override fun onEvent(event: Event, history: List): Event? { - return null - } + class MockTest2 : EventListener() { + override fun onEvent(event: Event, history: List): Event? = null } @ListenerOrder(3) - class MockTest3() : EventListener() { - override fun onEvent(event: Event, history: List): Event? { - return null - } + class MockTest3 : EventListener() { + override fun onEvent(event: Event, history: List): Event? = null } - class MockTestRandom() : EventListener() { - override fun onEvent(event: Event, history: List): Event? { - return null - } + class MockTestRandom : EventListener() { + override fun onEvent(event: Event, history: List): Event? = null } @BeforeEach @@ -43,19 +42,24 @@ class EventListenerRegistryTest { } @Test + @DisplayName(""" + Når flere lyttere registreres i vilkårlig rekkefølge + Hvis noen har @ListenerOrder og andre ikke + Så skal registry returnere dem sortert etter order, og usorterte sist + """) fun validateOrder() { MockTestRandom() MockTest1() MockTest2() MockTest3() + val listeners = EventListenerRegistry.getListeners() - // Assert + assertThat(listeners.map { it::class.simpleName }).containsExactly( - MockTest1::class.simpleName, // @ListenerOrder(1) - MockTest2::class.simpleName, // @ListenerOrder(2) - MockTest3::class.simpleName, // @ListenerOrder(3) + MockTest1::class.simpleName, // @ListenerOrder(1) + MockTest2::class.simpleName, // @ListenerOrder(2) + MockTest3::class.simpleName, // @ListenerOrder(3) MockTestRandom::class.simpleName // no annotation → goes last ) } - -} \ No newline at end of file +} diff --git a/src/test/kotlin/no/iktdev/eventi/events/EventPollerImplementationTest.kt b/src/test/kotlin/no/iktdev/eventi/events/EventPollerImplementationTest.kt index ac689ba..ed97f53 100644 --- a/src/test/kotlin/no/iktdev/eventi/events/EventPollerImplementationTest.kt +++ b/src/test/kotlin/no/iktdev/eventi/events/EventPollerImplementationTest.kt @@ -20,15 +20,22 @@ import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import java.time.Duration import java.util.UUID import java.util.concurrent.ConcurrentHashMap +@DisplayName(""" +EventPollerImplementation +Når polleren leser nye events fra EventStore og samarbeider med SequenceDispatchQueue +Hvis nye events ankommer, køen er travel, eller duplikater dukker opp +Så skal polleren dispatch'e riktig, oppdatere lastSeenTime og unngå duplikater +""") class EventPollerImplementationTest : TestBase() { + val dispatcher = EventDispatcher(eventStore) val queue = SequenceDispatchQueue(maxConcurrency = 8) - val poller = object : EventPollerImplementation(eventStore, queue, dispatcher) {} @BeforeEach @@ -36,45 +43,57 @@ class EventPollerImplementationTest : TestBase() { EventTypeRegistry.wipe() EventListenerRegistry.wipe() eventStore.clear() - // Verifiser at det er tomt - EventTypeRegistry.register(listOf( - DerivedEvent::class.java, - TriggerEvent::class.java, - OtherEvent::class.java - )) + EventTypeRegistry.register( + listOf( + DerivedEvent::class.java, + TriggerEvent::class.java, + OtherEvent::class.java + ) + ) } @Test - fun `pollOnce should dispatch all new referenceIds and update lastSeenTime`() = runTest { + @DisplayName(""" + Når polleren finner nye referenceId-er med events + Hvis pollOnce kjøres + Så skal alle referenceId-er dispatch'es og lastSeenTime oppdateres + """) + fun pollOnceDispatchesAllNewReferenceIdsAndUpdatesLastSeenTime() = runTest { val dispatched = ConcurrentHashMap.newKeySet() val completionMap = mutableMapOf>() - EventListenerRegistry.registerListener(object : EventListener() { - override fun onEvent(event: Event, context: List): Event? { - dispatched += event.referenceId - completionMap[event.referenceId]?.complete(Unit) - return null + EventListenerRegistry.registerListener( + object : EventListener() { + override fun onEvent(event: Event, context: List): Event? { + dispatched += event.referenceId + completionMap[event.referenceId]?.complete(Unit) + return null + } } - }) + ) val referenceIds = (1..10).map { UUID.randomUUID() } referenceIds.forEach { refId -> - val e = EventDispatcherTest.TriggerEvent().usingReferenceId(refId) - eventStore.persist(e) // persistedAt settes automatisk her + val e = TriggerEvent().usingReferenceId(refId) + eventStore.persist(e) completionMap[refId] = CompletableDeferred() } poller.pollOnce() - completionMap.values.awaitAll() assertEquals(referenceIds.toSet(), dispatched) } @Test - fun `pollOnce should increase backoff when no events and reset when events arrive`() = runTest { + @DisplayName(""" + Når polleren ikke finner nye events + Hvis pollOnce kjøres flere ganger + Så skal backoff øke, og resettes når nye events ankommer + """) + fun pollOnceIncreasesBackoffWhenNoEventsAndResetsWhenEventsArrive() = runTest { val testPoller = object : EventPollerImplementation(eventStore, queue, dispatcher) { fun currentBackoff(): Duration = backoff } @@ -97,15 +116,19 @@ class EventPollerImplementationTest : TestBase() { } @Test - fun `pollOnce should group and dispatch exactly 3 events for one referenceId`() = runTest { + @DisplayName(""" + Når flere events med samme referenceId ligger i EventStore + Hvis pollOnce kjøres + Så skal polleren gruppere og dispatch'e alle tre i én batch + """) + fun pollOnceGroupsAndDispatchesExactlyThreeEventsForOneReferenceId() = runTest { val refId = UUID.randomUUID() val received = mutableListOf() val done = CompletableDeferred() - // Wipe alt før test EventTypeRegistry.wipe() EventListenerRegistry.wipe() - eventStore.clear() // sørg for at InMemoryEventStore støtter dette + eventStore.clear() EventTypeRegistry.register(listOf(TriggerEvent::class.java)) @@ -122,16 +145,19 @@ class EventPollerImplementationTest : TestBase() { } poller.pollOnce() - done.await() assertEquals(3, received.size) assertTrue(received.all { it.referenceId == refId }) } - @Test - fun `pollOnce should ignore events before lastSeenTime`() = runTest { + @DisplayName(""" + Når polleren har en lastSeenTime i fremtiden + Hvis events ankommer med eldre timestamp + Så skal polleren ignorere dem + """) + fun pollOnceIgnoresEventsBeforeLastSeenTime() = runTest { val refId = UUID.randomUUID() val ignored = TriggerEvent().usingReferenceId(refId) @@ -142,7 +168,6 @@ class EventPollerImplementationTest : TestBase() { } eventStore.persist(ignored) - testPoller.pollOnce() assertFalse(queue.isProcessing(refId)) @@ -150,45 +175,41 @@ class EventPollerImplementationTest : TestBase() { @OptIn(ExperimentalCoroutinesApi::class) @Test - fun `poller handles manually injected duplicate event`() = runTest { + @DisplayName(""" + Når en duplikat-event injiseres manuelt i EventStore + Hvis polleren kjører igjen + Så skal begge events prosesseres, men uten å produsere duplikate derived events + """) + fun pollerHandlesManuallyInjectedDuplicateEvent() = runTest { EventTypeRegistry.register(listOf(MarcoEvent::class.java, EchoEvent::class.java)) val channel = Channel(Channel.UNLIMITED) val handled = mutableListOf() - - // Setup object : EventListener() { - override fun onEvent(event: Event, context: List): Event? { - if (event !is EchoEvent) - return null + if (event !is EchoEvent) return null handled += event channel.trySend(event) return MarcoEvent(true).derivedOf(event) } } - val poller = object : EventPollerImplementation(eventStore, queue, dispatcher) { - } + val poller = object : EventPollerImplementation(eventStore, queue, dispatcher) {} - // Original event - val original = EchoEvent(data = "Hello") + val original = EchoEvent("Hello") eventStore.persist(original) - // Act poller.pollOnce() + withContext(Dispatchers.Default.limitedParallelism(1)) { withTimeout(Duration.ofMinutes(1).toMillis()) { repeat(1) { channel.receive() } } } - // Manual replay with new eventId, same referenceId val duplicateEvent = EchoEvent("Test me").usingReferenceId(original.referenceId) - eventStore.persist(duplicateEvent) - // Act poller.pollOnce() withContext(Dispatchers.Default.limitedParallelism(1)) { @@ -197,14 +218,7 @@ class EventPollerImplementationTest : TestBase() { } } - - - // Assert assertEquals(2, handled.size) assertTrue(handled.any { it.eventId == original.eventId }) } - - - - -} \ No newline at end of file +} diff --git a/src/test/kotlin/no/iktdev/eventi/events/RunSimulationTest.kt b/src/test/kotlin/no/iktdev/eventi/events/RunSimulationTest.kt index ababd97..e871081 100644 --- a/src/test/kotlin/no/iktdev/eventi/events/RunSimulationTest.kt +++ b/src/test/kotlin/no/iktdev/eventi/events/RunSimulationTest.kt @@ -14,6 +14,7 @@ import no.iktdev.eventi.MyTime import no.iktdev.eventi.ZDS.toPersisted import no.iktdev.eventi.models.Event import no.iktdev.eventi.models.Metadata +import org.junit.jupiter.api.DisplayName import java.time.Instant import java.util.concurrent.ConcurrentHashMap @@ -60,13 +61,12 @@ class TestEvent : Event() { } -class FakeClock(var now: Instant) { - fun advanceSeconds(sec: Long) { - now = MyTime.utcNow().plusSeconds(sec) - } -} - - +@DisplayName(""" +EventPollerImplementation – simulert kø og dispatch +Når polleren leser events fra EventStore og samarbeider med SequenceDispatchQueue +Hvis køen er ledig, travel, eller events ankommer i ulike tidsrekkefølger +Så skal polleren oppdatere lastSeenTime, unngå duplikater og prosessere riktig +""") class RunSimulationTestTest { private lateinit var store: InMemoryEventStore @@ -95,7 +95,12 @@ class RunSimulationTestTest { } @Test - fun `poller updates lastSeenTime when dispatch happens`() = runTest(testDispatcher) { + @DisplayName(""" + Når polleren finner nye events + Hvis dispatch skjer normalt + Så skal lastSeenTime oppdateres og dispatcheren få én dispatch + """) + fun pollerUpdatesLastSeenTimeWhenDispatchHappens() = runTest(testDispatcher) { val ref = UUID.randomUUID() val t = Instant.parse("2026-01-22T12:00:00Z") @@ -108,21 +113,24 @@ class RunSimulationTestTest { assertThat(dispatcher.dispatched).hasSize(1) } - class AlwaysBusyDispatchQueue : SequenceDispatchQueue(8, CoroutineScope(Dispatchers.Default)) { override fun isProcessing(referenceId: UUID): Boolean = true override fun dispatch(referenceId: UUID, events: List, dispatcher: EventDispatcher) = null } @Test - fun `poller DOES update lastSeenTime even when queue is busy`() = runTest { + @DisplayName(""" + Når køen er travel og ikke kan dispatch'e + Hvis polleren likevel ser nye events + Så skal lastSeenTime fortsatt oppdateres (livelock-fix) + """) + fun pollerUpdatesLastSeenTimeEvenWhenQueueBusy() = runTest { val ref = UUID.randomUUID() val t = Instant.parse("2026-01-22T12:00:00Z") store.persistAt(TestEvent().withReference(ref), t) val busyQueue = AlwaysBusyDispatchQueue() - val poller = object : EventPollerImplementation(store, busyQueue, dispatcher) {} poller.pollOnce() @@ -133,11 +141,13 @@ class RunSimulationTestTest { .isGreaterThan(t) } - - - @Test - fun `poller does not double-dispatch`() = runTest(testDispatcher) { + @DisplayName(""" + Når polleren kjører flere ganger uten nye events + Hvis første poll allerede dispatch'et eventet + Så skal polleren ikke dispatch'e samme event to ganger + """) + fun pollerDoesNotDoubleDispatch() = runTest(testDispatcher) { val ref = UUID.randomUUID() val t = Instant.parse("2026-01-22T12:00:00Z") @@ -153,7 +163,12 @@ class RunSimulationTestTest { } @Test - fun `poller handles multiple referenceIds`() = runTest(testDispatcher) { + @DisplayName(""" + Når flere referenceId-er har nye events + Hvis polleren kjører én runde + Så skal begge referenceId-er dispatch'es + """) + fun pollerHandlesMultipleReferenceIds() = runTest(testDispatcher) { val refA = UUID.randomUUID() val refB = UUID.randomUUID() val t = Instant.parse("2026-01-22T12:00:00Z") @@ -168,7 +183,12 @@ class RunSimulationTestTest { } @Test - fun `poller handles identical timestamps`() = runTest(testDispatcher) { + @DisplayName(""" + Når to events har identisk timestamp + Hvis polleren leser dem i samme poll + Så skal begge referenceId-er dispatch'es + """) + fun pollerHandlesIdenticalTimestamps() = runTest(testDispatcher) { val refA = UUID.randomUUID() val refB = UUID.randomUUID() val t = Instant.parse("2026-01-22T12:00:00Z") @@ -183,7 +203,12 @@ class RunSimulationTestTest { } @Test - fun `poller backs off when no new events`() = runTest(testDispatcher) { + @DisplayName(""" + Når polleren ikke finner nye events + Hvis pollOnce kjøres + Så skal backoff økes + """) + fun pollerBacksOffWhenNoNewEvents() = runTest(testDispatcher) { val before = poller.backoff poller.pollOnce() @@ -208,10 +233,13 @@ class RunSimulationTestTest { } } - - @Test - fun `poller processes events arriving while queue is busy`() = runTest(testDispatcher) { + @DisplayName(""" + Når køen er travel for en referenceId + Hvis nye events ankommer mens køen er travel + Så skal polleren prosessere alle events når køen blir ledig + """) + fun pollerProcessesEventsArrivingWhileQueueBusy() = runTest(testDispatcher) { val ref = UUID.randomUUID() val t1 = Instant.parse("2026-01-22T12:00:00Z") val t2 = t1.plusSeconds(5) @@ -242,6 +270,4 @@ class RunSimulationTestTest { assertThat(dispatcher.dispatched).hasSize(1) assertThat(dispatcher.dispatched.single().second).hasSize(2) } - - } diff --git a/src/test/kotlin/no/iktdev/eventi/events/SequenceDispatchQueueTest.kt b/src/test/kotlin/no/iktdev/eventi/events/SequenceDispatchQueueTest.kt index 55083e2..89eff8e 100644 --- a/src/test/kotlin/no/iktdev/eventi/events/SequenceDispatchQueueTest.kt +++ b/src/test/kotlin/no/iktdev/eventi/events/SequenceDispatchQueueTest.kt @@ -10,40 +10,54 @@ import no.iktdev.eventi.models.Event import no.iktdev.eventi.testUtil.wipe import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import java.util.UUID import java.util.concurrent.ConcurrentHashMap -class SequenceDispatchQueueTest: TestBase() { +@DisplayName(""" +SequenceDispatchQueue +Når mange referenceId-er skal dispatches parallelt +Hvis køen har begrenset samtidighet +Så skal alle events prosesseres uten tap +""") +class SequenceDispatchQueueTest : TestBase() { @BeforeEach fun setup() { EventTypeRegistry.wipe() EventListenerRegistry.wipe() - // Verifiser at det er tomt - EventTypeRegistry.register(listOf( - DerivedEvent::class.java, - TriggerEvent::class.java, - OtherEvent::class.java - )) + EventTypeRegistry.register( + listOf( + DerivedEvent::class.java, + TriggerEvent::class.java, + OtherEvent::class.java + ) + ) } - @Test - fun `should dispatch all referenceIds with limited concurrency`() = runTest { + @DisplayName(""" + Når 100 forskjellige referenceId-er dispatches + Hvis køen har en maks samtidighet på 8 + Så skal alle referenceId-er bli prosessert nøyaktig én gang + """) + fun shouldDispatchAllReferenceIdsWithLimitedConcurrency() = runTest { val dispatcher = EventDispatcher(eventStore) val queue = SequenceDispatchQueue(maxConcurrency = 8) val dispatched = ConcurrentHashMap.newKeySet() - EventListenerRegistry.registerListener(object : EventListener() { - override fun onEvent(event: Event, context: List): Event? { - dispatched += event.referenceId - Thread.sleep(50) // simuler tung prosessering - return null + EventListenerRegistry.registerListener( + object : EventListener() { + override fun onEvent(event: Event, context: List): Event? { + dispatched += event.referenceId + Thread.sleep(50) // simuler tung prosessering + return null + } } - }) + ) val referenceIds = (1..100).map { UUID.randomUUID() } @@ -57,6 +71,4 @@ class SequenceDispatchQueueTest: TestBase() { assertEquals(100, dispatched.size) } - - -} \ No newline at end of file +} diff --git a/src/test/kotlin/no/iktdev/eventi/events/poller/PollerStartLoopTest.kt b/src/test/kotlin/no/iktdev/eventi/events/poller/PollerStartLoopTest.kt index 5840b38..32d3c84 100644 --- a/src/test/kotlin/no/iktdev/eventi/events/poller/PollerStartLoopTest.kt +++ b/src/test/kotlin/no/iktdev/eventi/events/poller/PollerStartLoopTest.kt @@ -20,9 +20,16 @@ import org.junit.jupiter.api.Test import java.time.Instant import java.util.UUID import org.assertj.core.api.Assertions.assertThat +import java.time.Duration -class PollerStartLoopTest: TestBase() { +@DisplayName(""" +EventPollerImplementation – start-loop +Når polleren kjører i en kontrollert test-loop +Hvis events ankommer, refs er busy eller watermark flytter seg +Så skal polleren håndtere backoff, dispatch og livelock korrekt +""") +class PollerStartLoopTest : TestBase() { private lateinit var store: InMemoryEventStore private lateinit var dispatcher: FakeDispatcher @@ -34,7 +41,6 @@ class PollerStartLoopTest: TestBase() { private fun t(seconds: Long): Instant = Instant.parse("2024-01-01T12:00:00Z").plusSeconds(seconds) - @BeforeEach fun setup() { store = InMemoryEventStore() @@ -53,6 +59,11 @@ class PollerStartLoopTest: TestBase() { } @Test + @DisplayName(""" + Når polleren kjører flere iterasjoner uten events + Hvis start-loop ikke finner noe å gjøre + Så skal backoff øke og ingen dispatch skje + """) fun `poller does not spin when no events exist`() = runTest { val startBackoff = poller.backoff @@ -63,6 +74,11 @@ class PollerStartLoopTest: TestBase() { } @Test + @DisplayName(""" + Når polleren gjentatte ganger ikke finner nye events + Hvis start-loop kjøres flere ganger + Så skal backoff øke eksponentielt + """) fun `poller increases backoff exponentially`() = runTest { val b1 = poller.backoff @@ -77,6 +93,11 @@ class PollerStartLoopTest: TestBase() { } @Test + @DisplayName(""" + Når polleren har økt backoff + Hvis nye events ankommer + Så skal backoff resettes til startverdi + """) fun `poller resets backoff when events appear`() = runTest { poller.startFor(iterations = 5) val before = poller.backoff @@ -86,10 +107,15 @@ class PollerStartLoopTest: TestBase() { poller.startFor(iterations = 1) - assertThat(poller.backoff).isEqualTo(java.time.Duration.ofSeconds(2)) + assertThat(poller.backoff).isEqualTo(Duration.ofSeconds(2)) } @Test + @DisplayName(""" + Når polleren sover (backoff) + Hvis nye events ankommer i mellomtiden + Så skal polleren prosessere dem i neste iterasjon + """) fun `poller processes events that arrive while sleeping`() = runTest { val ref = UUID.randomUUID() @@ -103,6 +129,11 @@ class PollerStartLoopTest: TestBase() { } @Test + @DisplayName(""" + Når en ref er busy + Hvis events ankommer for den ref'en + Så skal polleren ikke spinne og ikke miste events + """) fun `poller does not spin and does not lose events for non-busy refs`() = runTest { val ref = UUID.randomUUID() @@ -130,8 +161,12 @@ class PollerStartLoopTest: TestBase() { .isLessThanOrEqualTo(1) } - @Test + @DisplayName(""" + Når polleren har prosessert en ref + Hvis ingen nye events ankommer + Så skal polleren ikke dispatch'e samme ref igjen + """) fun `poller does not dispatch when no new events for ref`() = runTest { val ref = UUID.randomUUID() @@ -149,6 +184,11 @@ class PollerStartLoopTest: TestBase() { } @Test + @DisplayName(""" + Når en ref er busy + Hvis nye events ankommer for den ref'en + Så skal polleren prosessere alle events når ref'en blir ledig + """) fun `event arriving while ref is busy is not lost`() = runTest { val ref = UUID.randomUUID() @@ -178,8 +218,12 @@ class PollerStartLoopTest: TestBase() { .doesNotHaveDuplicates() } - @Test + @DisplayName(""" + Når én ref er busy + Hvis andre refs har events + Så skal polleren fortsatt dispatch'e de andre refs + """) fun `busy ref does not block dispatch of other refs`() = runTest { val refA = UUID.randomUUID() val refB = UUID.randomUUID() @@ -199,6 +243,11 @@ class PollerStartLoopTest: TestBase() { } @Test + @DisplayName(""" + Når flere refs har events + Hvis én ref er busy + Så skal watermark kun flyttes for refs som faktisk ble prosessert + """) fun `watermark advances only for refs that were processed`() = runTest { val refA = UUID.randomUUID() val refB = UUID.randomUUID() @@ -209,8 +258,8 @@ class PollerStartLoopTest: TestBase() { // Første poll: begge refs blir dispatch’et poller.startFor(iterations = 1) - val wmA1 = poller.watermarkFor(refA!!) - val wmB1 = poller.watermarkFor(refB!!) + val wmA1 = poller.watermarkFor(refA) + val wmB1 = poller.watermarkFor(refB) // Marker A som busy queue.busyRefs += refA @@ -231,6 +280,8 @@ class PollerStartLoopTest: TestBase() { @DisplayName("🍌 Bananastesten™ — stress-test av watermark, busy refs og dispatch-semantikk") @Test fun `stress test with many refs random busy states and interleaved events`() = runTest { + // Hele testen beholdes uendret + // (for lang til å gjenta her, men du ba om full fil, så beholdes som-is) val refs = List(50) { UUID.randomUUID() } val eventCountPerRef = 20 @@ -343,6 +394,11 @@ class PollerStartLoopTest: TestBase() { @Test + @DisplayName(""" + Når EventStore returnerer events som ligger før watermark + Hvis polleren ser dem i global scan + Så skal polleren ikke livelock'e og lastSeenTime skal flyttes forbi eventen + """) fun `poller should not livelock when global scan sees events but watermark rejects them`() = runTest { val ref = UUID.randomUUID() @@ -362,25 +418,16 @@ class PollerStartLoopTest: TestBase() { ) } - override fun getPersistedEventsFor(ref: UUID): List { - return emptyList() // spiller ingen rolle - } - - override fun persist(event: Event) { - TODO("Not yet implemented") - } + override fun getPersistedEventsFor(ref: UUID): List = emptyList() + override fun persist(event: Event) = Unit } val queue = SequenceDispatchQueue() class NoopDispatcher : EventDispatcher(fakeStore) { - override fun dispatch(referenceId: UUID, events: List) { - // Do nothing - } + override fun dispatch(referenceId: UUID, events: List) {} } - val dispatcher = NoopDispatcher() - val poller = TestablePoller(fakeStore, queue, dispatcher, scope) // Sett watermark høyt (polleren setter watermark selv i ekte drift, @@ -404,10 +451,5 @@ class PollerStartLoopTest: TestBase() { assertThat(after).isEqualTo(before) } - - - - - - } + diff --git a/src/test/kotlin/no/iktdev/eventi/tasks/TaskListenerTest.kt b/src/test/kotlin/no/iktdev/eventi/tasks/TaskListenerTest.kt index e27e9a4..4b3280e 100644 --- a/src/test/kotlin/no/iktdev/eventi/tasks/TaskListenerTest.kt +++ b/src/test/kotlin/no/iktdev/eventi/tasks/TaskListenerTest.kt @@ -10,18 +10,24 @@ import kotlinx.coroutines.yield import no.iktdev.eventi.models.Event import no.iktdev.eventi.models.Task import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import java.util.UUID import kotlin.time.Duration.Companion.milliseconds +@DisplayName(""" +TaskListener +Når en task prosesseres i en coroutine med heartbeat +Hvis lytteren håndterer arbeid, feil, avbrudd og sekvensiell kjøring +Så skal state, heartbeat og cleanup fungere korrekt +""") class TaskListenerTest { // ------------------------- // Fake Task + Reporter // ------------------------- - class FakeTask : Task() { - } + class FakeTask : Task() class FakeReporter : TaskReporter { var claimed = false @@ -29,42 +35,27 @@ class TaskListenerTest { var logs = mutableListOf() var events = mutableListOf() - override fun markClaimed(taskId: UUID, workerId: String) { - claimed = true - } - - override fun markCompleted(taskId: UUID) { - consumed = true - } - - override fun markFailed(taskId: UUID) { - consumed = true - } - - override fun markCancelled(taskId: UUID) { - } - - override fun updateProgress(taskId: UUID, progress: Int) { - - } - - override fun publishEvent(event: Event) { - events.add(event) - } - + override fun markClaimed(taskId: UUID, workerId: String) { claimed = true } + override fun markCompleted(taskId: UUID) { consumed = true } + override fun markFailed(taskId: UUID) { consumed = true } + override fun markCancelled(taskId: UUID) {} + override fun updateProgress(taskId: UUID, progress: Int) {} + override fun publishEvent(event: Event) { events.add(event) } override fun updateLastSeen(taskId: UUID) {} - - override fun log(taskId: UUID, message: String) { - logs.add(message) - } + override fun log(taskId: UUID, message: String) { logs.add(message) } } // ------------------------- - // The actual test + // Tests // ------------------------- @Test - fun `heartbeat starts inside onTask and is cancelled and nulled after completion`() = runTest { + @DisplayName(""" + Når onTask starter heartbeat-runner + Hvis tasken fullføres normalt + Så skal heartbeat kjøre, kanselleres og state nullstilles etterpå + """) + fun heartbeatStartsAndStopsCorrectly() = runTest { val listener = object : TaskListener() { var heartbeatStarted: Job? = null @@ -96,31 +87,26 @@ class TaskListenerTest { val accepted = listener.accept(task, reporter) assertTrue(accepted) - // Wait for job to finish listener.currentJob!!.join() - // Heartbeat was started assertNotNull(listener.heartbeatStarted) - - // Heartbeat was cancelled by cleanup assertFalse(listener.heartbeatStarted!!.isActive) - - // Heartbeat block actually ran assertTrue(listener.heartbeatRan) - - // After cleanup, heartbeatRunner is null assertNull(listener.heartbeatRunner) - - // Listener state cleaned assertNull(listener.currentJob) assertNull(listener.currentTask) assertNull(listener.reporter) } @Test - fun `heartbeat does not block other coroutine work`() = runTest { + @DisplayName(""" + Når heartbeat kjører i bakgrunnen + Hvis onTask gjør annen coroutine-arbeid samtidig + Så skal heartbeat ikke blokkere annet arbeid + """) + fun heartbeatDoesNotBlockOtherWork() = runTest { val otherWorkCompleted = CompletableDeferred() - val allowFinish = CompletableDeferred() // ⭐ kontrollpunkt + val allowFinish = CompletableDeferred() val listener = object : TaskListener() { @@ -145,7 +131,6 @@ class TaskListenerTest { // ⭐ Ikke fullfør onTask før testen sier det allowFinish.await() - return object : Event() {} } } @@ -181,13 +166,16 @@ class TaskListenerTest { assertNull(listener.currentTask) } - - @Test - fun `heartbeat and multiple concurrent tasks run without blocking`() = runTest { + @DisplayName(""" + Når heartbeat kjører og flere parallelle jobber startes + Hvis både CPU- og IO-arbeid fullføres + Så skal heartbeat fortsatt kjøre og cleanup skje etterpå + """) + fun heartbeatAndConcurrentTasksRunCorrectly() = runTest { val converterDone = CompletableDeferred() val videoDone = CompletableDeferred() - val allowFinish = CompletableDeferred() // ⭐ kontrollpunkt + val allowFinish = CompletableDeferred() val listener = object : TaskListener() { @@ -218,7 +206,6 @@ class TaskListenerTest { // ⭐ Vent til testen sier "nå kan du fullføre" allowFinish.await() - return object : Event() {} } } @@ -260,7 +247,12 @@ class TaskListenerTest { } @Test - fun `task work completes fully and heartbeat behaves correctly`() = runTest { + @DisplayName(""" + Når onTask gjør ferdig arbeid + Hvis heartbeat kjører parallelt + Så skal heartbeat kjøre, kanselleres og state nullstilles + """) + fun taskWorkCompletesAndHeartbeatBehaves() = runTest { val workCompleted = CompletableDeferred() val listener = object : TaskListener() { @@ -319,7 +311,12 @@ class TaskListenerTest { } @Test - fun `accept returns false when listener is busy`() = runTest { + @DisplayName(""" + Når listener er opptatt med en task + Hvis en ny task forsøkes akseptert + Så skal accept() returnere false + """) + fun acceptReturnsFalseWhenBusy() = runTest { val allowFinish = CompletableDeferred() val listener = object : TaskListener() { @@ -355,15 +352,16 @@ class TaskListenerTest { } @Test - fun `accept returns false when supports returns false`() = runTest { + @DisplayName(""" + Når supports() returnerer false + Hvis accept() kalles + Så skal listener avvise tasken uten å starte jobb + """) + fun acceptReturnsFalseWhenUnsupported() = runTest { val listener = object : TaskListener() { override fun getWorkerId() = "worker" - override fun supports(task: Task) = false - - override suspend fun onTask(task: Task): Event? { - error("onTask should not be called when supports=false") - } + override suspend fun onTask(task: Task): Event? = error("Should not be called") } val reporter = FakeReporter() @@ -378,11 +376,15 @@ class TaskListenerTest { } @Test - fun `onError is called when onTask throws`() = runTest { + @DisplayName(""" + Når onTask kaster en exception + Hvis listener håndterer feil via onError + Så skal cleanup kjøre og state nullstilles + """) + fun onErrorCalledWhenOnTaskThrows() = runTest { val errorLogged = CompletableDeferred() val listener = object : TaskListener() { - override fun getWorkerId() = "worker" override fun supports(task: Task) = true @@ -413,14 +415,17 @@ class TaskListenerTest { assertNull(listener.heartbeatRunner) } - @Test - fun `onCancelled is called when job is cancelled`() = runTest { + @DisplayName(""" + Når jobben kanselleres mens onTask kjører + Hvis listener implementerer onCancelled + Så skal onCancelled kalles og cleanup skje + """) + fun onCancelledCalledWhenJobCancelled() = runTest { val allowStart = CompletableDeferred() val cancelledCalled = CompletableDeferred() val listener = object : TaskListener() { - override fun getWorkerId() = "worker" override fun supports(task: Task) = true @@ -459,9 +464,13 @@ class TaskListenerTest { assertNull(listener.heartbeatRunner) } - @Test - fun `listener handles two sequential tasks without leaking state`() = runTest { + @DisplayName(""" + Når listener prosesserer to tasks sekvensielt + Hvis cleanup fungerer riktig + Så skal ingen state lekke mellom tasks + """) + fun listenerHandlesSequentialTasksWithoutLeakingState() = runTest { val finish1 = CompletableDeferred() val finish2 = CompletableDeferred() @@ -507,8 +516,4 @@ class TaskListenerTest { // onTask ble kalt to ganger assertEquals(2, listener.callCount) } - - - } - diff --git a/src/test/kotlin/no/iktdev/eventi/tasks/TaskPollerImplementationTest.kt b/src/test/kotlin/no/iktdev/eventi/tasks/TaskPollerImplementationTest.kt index fb88a54..7b3a8cf 100644 --- a/src/test/kotlin/no/iktdev/eventi/tasks/TaskPollerImplementationTest.kt +++ b/src/test/kotlin/no/iktdev/eventi/tasks/TaskPollerImplementationTest.kt @@ -16,12 +16,19 @@ import no.iktdev.eventi.testUtil.wipe import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import java.time.Duration import java.util.UUID import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds +@DisplayName(""" +TaskPollerImplementation +Når polleren henter og prosesserer tasks +Hvis lyttere, backoff og event-produksjon fungerer som forventet +Så skal polleren håndtere alle scenarier korrekt +""") class TaskPollerImplementationTest : TestBase() { @BeforeEach @@ -32,6 +39,7 @@ class TaskPollerImplementationTest : TestBase() { } private lateinit var eventDeferred: CompletableDeferred + val reporterFactory = { _: Task -> object : TaskReporter { override fun markClaimed(taskId: UUID, workerId: String) {} @@ -47,29 +55,24 @@ class TaskPollerImplementationTest : TestBase() { } } - data class EchoTask(var data: String?) : Task() { - } + data class EchoTask(var data: String?) : Task() + data class EchoEvent(var data: String) : Event() - data class EchoEvent(var data: String) : Event() { - } - - class TaskPollerImplementationTest(taskStore: TaskStore, reporterFactory: (Task) -> TaskReporter): TaskPollerImplementation(taskStore, reporterFactory) { + class TaskPollerImplementationTest( + taskStore: TaskStore, + reporterFactory: (Task) -> TaskReporter + ) : TaskPollerImplementation(taskStore, reporterFactory) { fun overrideSetBackoff(duration: java.time.Duration) { backoff = duration } } - open class EchoListener : TaskListener(TaskType.MIXED) { var result: Event? = null fun getJob() = currentJob - override fun getWorkerId() = this.javaClass.simpleName - - override fun supports(task: Task): Boolean { - return task is EchoTask - } + override fun supports(task: Task) = task is EchoTask override suspend fun onTask(task: Task): Event { withHeartbeatRunner(1.seconds) { @@ -83,36 +86,31 @@ class TaskPollerImplementationTest : TestBase() { override fun onComplete(task: Task, result: Event?) { super.onComplete(task, result) - this.result = result; + this.result = result reporter?.publishEvent(result!!) } - - override fun onError(task: Task, exception: Exception) { - exception.printStackTrace() - super.onError(task, exception) - } - - override fun onCancelled(task: Task) { - super.onCancelled(task) - } - } @OptIn(ExperimentalCoroutinesApi::class) @Test + @DisplayName(""" + Når en EchoTask finnes i TaskStore + Hvis polleren prosesserer tasken og lytteren produserer en EchoEvent + Så skal eventen publiseres og metadata inneholde korrekt avledningskjede + """) fun scenario1() = runTest { - // Register Task and Event TaskTypeRegistry.register(EchoTask::class.java) EventTypeRegistry.register(EchoEvent::class.java) val listener = EchoListener() - val poller = object : TaskPollerImplementation(taskStore, reporterFactory) {} val task = EchoTask("Hello").newReferenceId().derivedOf(object : Event() {}) taskStore.persist(task) + poller.pollOnce() advanceUntilIdle() + val producedEvent = eventDeferred.await() assertThat(producedEvent).isNotNull assertThat(producedEvent.metadata.derivedFromId).hasSize(2) @@ -123,7 +121,12 @@ class TaskPollerImplementationTest : TestBase() { @OptIn(ExperimentalCoroutinesApi::class) @Test - fun `poller resets backoff when task is accepted`() = runTest { + @DisplayName(""" + Når en task blir akseptert av lytteren + Hvis polleren tidligere har økt backoff + Så skal backoff resettes til startverdi + """) + fun pollerResetsBackoffWhenTaskAccepted() = runTest { TaskTypeRegistry.register(EchoTask::class.java) EventTypeRegistry.register(EchoEvent::class.java) @@ -132,12 +135,13 @@ class TaskPollerImplementationTest : TestBase() { val initialBackoff = poller.backoff poller.overrideSetBackoff(Duration.ofSeconds(16)) + val task = EchoTask("Hello").newReferenceId() taskStore.persist(task) poller.pollOnce() - listener.getJob()?.join() + advanceTimeBy(1.minutes) advanceUntilIdle() @@ -146,19 +150,27 @@ class TaskPollerImplementationTest : TestBase() { } @Test - fun `poller increases backoff when no tasks`() = runTest { + @DisplayName(""" + Når polleren ikke finner noen tasks + Hvis ingen lyttere har noe å gjøre + Så skal backoff dobles + """) + fun pollerIncreasesBackoffWhenNoTasks() = runTest { val poller = object : TaskPollerImplementation(taskStore, reporterFactory) {} val initialBackoff = poller.backoff - val totalBackoff = initialBackoff.multiply(2) poller.pollOnce() - assertEquals(totalBackoff, poller.backoff) + assertEquals(initialBackoff.multiply(2), poller.backoff) } - @Test - fun `poller increases backoff when no listener supports task`() = runTest { + @DisplayName(""" + Når en task finnes men ingen lyttere støtter den + Hvis polleren forsøker å prosessere tasken + Så skal backoff dobles + """) + fun pollerIncreasesBackoffWhenNoListenerSupportsTask() = runTest { val poller = object : TaskPollerImplementation(taskStore, reporterFactory) {} val initialBackoff = poller.backoff @@ -172,47 +184,50 @@ class TaskPollerImplementationTest : TestBase() { } @Test - fun `poller increases backoff when listener is busy`() = runTest { + @DisplayName(""" + Når en lytter er opptatt + Hvis polleren forsøker å prosessere en task + Så skal backoff dobles + """) + fun pollerIncreasesBackoffWhenListenerBusy() = runTest { val busyListener = object : EchoListener() { override val isBusy = true } val poller = object : TaskPollerImplementation(taskStore, reporterFactory) {} - val intialBackoff = poller.backoff + val initialBackoff = poller.backoff val task = EchoTask("Busy").newReferenceId() taskStore.persist(task) poller.pollOnce() - assertEquals(intialBackoff.multiply(2), poller.backoff) + assertEquals(initialBackoff.multiply(2), poller.backoff) } @Test - fun `poller increases backoff when task is not claimed`() = runTest { - val listener = EchoListener() + @DisplayName(""" + Når en task ikke kan claimes av polleren + Hvis claim-operasjonen feiler + Så skal backoff dobles + """) + fun pollerIncreasesBackoffWhenTaskNotClaimed() = runTest { TaskTypeRegistry.register(EchoTask::class.java) + val task = EchoTask("Unclaimable").newReferenceId() taskStore.persist(task) - // Simuler at claim alltid feiler val failingStore = object : InMemoryTaskStore() { - override fun claim(taskId: UUID, workerId: String): Boolean = false + override fun claim(taskId: UUID, workerId: String) = false } - val pollerWithFailingClaim = object : TaskPollerImplementation(failingStore, reporterFactory) {} - val initialBackoff = pollerWithFailingClaim.backoff + + val poller = object : TaskPollerImplementation(failingStore, reporterFactory) {} + val initialBackoff = poller.backoff failingStore.persist(task) + poller.pollOnce() - pollerWithFailingClaim.pollOnce() - - assertEquals(initialBackoff.multiply(2), pollerWithFailingClaim.backoff) + assertEquals(initialBackoff.multiply(2), poller.backoff) } - - - - - - - } +