✨ Only send an email if previous state was not a failure, too
authorDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Sat, 29 Apr 2023 19:39:49 +0000 (21:39 +0200)
committerDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Sat, 29 Apr 2023 19:39:49 +0000 (21:39 +0200)
src/main/java/net/pterodactylus/rhynodge/engine/ReactionRunner.java
src/test/kotlin/net/pterodactylus/rhynodge/engine/ReactionRunnerTest.kt [new file with mode: 0644]

index 0538a0f..45642ce 100644 (file)
@@ -49,8 +49,11 @@ public class ReactionRunner implements Runnable {
                state = runStateThroughFilters(state);
                if (!state.success()) {
                        logger.info(format("Reaction %s failed in %s.", reaction.name(), state));
+                       Optional<State> lastState = reactionState.loadLastState();
                        saveStateWithIncreasedFailCount(state);
-                       errorEmailAction.execute(createErrorOutput(reaction, state));
+                       if (thisFailureIsTheFirstFailure(lastState)) {
+                               errorEmailAction.execute(createErrorOutput(reaction, state));
+                       }
                        return;
                }
                Optional<State> lastSuccessfulState = reactionState.loadLastSuccessfulState();
@@ -69,6 +72,10 @@ public class ReactionRunner implements Runnable {
                logger.info(format("Reaction %s finished.", reaction.name()));
        }
 
+       private static boolean thisFailureIsTheFirstFailure(Optional<State> lastState) {
+               return lastState.map(State::success).orElse(true);
+       }
+
        private void saveStateWithIncreasedFailCount(State state) {
                Optional<State> lastState = reactionState.loadLastState();
                state.setFailCount(lastState.map(State::failCount).orElse(0) + 1);
diff --git a/src/test/kotlin/net/pterodactylus/rhynodge/engine/ReactionRunnerTest.kt b/src/test/kotlin/net/pterodactylus/rhynodge/engine/ReactionRunnerTest.kt
new file mode 100644 (file)
index 0000000..4dfd891
--- /dev/null
@@ -0,0 +1,116 @@
+package net.pterodactylus.rhynodge.engine
+
+import net.pterodactylus.rhynodge.Query
+import net.pterodactylus.rhynodge.Reaction
+import net.pterodactylus.rhynodge.State
+import net.pterodactylus.rhynodge.actions.EmailAction
+import net.pterodactylus.rhynodge.output.Output
+import net.pterodactylus.rhynodge.states.AbstractState
+import net.pterodactylus.rhynodge.states.FailedState
+import net.pterodactylus.rhynodge.states.StateManager
+import org.hamcrest.Description
+import org.hamcrest.Matcher
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.hamcrest.TypeSafeDiagnosingMatcher
+import org.junit.Test
+import java.util.Optional
+import java.util.Optional.empty
+import java.util.Optional.of
+import java.util.concurrent.atomic.AtomicBoolean
+import java.util.concurrent.atomic.AtomicReference
+
+/**
+ * Unit test for [ReactionRunner].
+ */
+class ReactionRunnerTest {
+
+       @Test
+       fun `reaction runner sends email if reaction fails and there is no last state`() {
+               val emailSent = AtomicBoolean(false)
+               val errorEmailAction = createEmailAction { emailSent.set(true) }
+               val reactionRunner = ReactionRunner(failingReaction, reactionState, errorEmailAction)
+               reactionRunner.run()
+               assertThat(emailSent.get(), equalTo(true))
+       }
+
+       @Test
+       fun `reaction runner sends email if reaction fails and the last state is successful`() {
+               val reactionState = createReactionState({
+                       of<State>(object : AbstractState(true) {
+                               override fun plainText() = "success"
+                       })
+               })
+               val emailSent = AtomicBoolean(false)
+               val errorEmailAction = createEmailAction { emailSent.set(true) }
+               val reactionRunner = ReactionRunner(failingReaction, reactionState, errorEmailAction)
+               reactionRunner.run()
+               assertThat(emailSent.get(), equalTo(true))
+       }
+
+       @Test
+       fun `reaction runner does not send email if reaction fails and the last state is failed`() {
+               val reactionState = createReactionState({ of<State>(FailedState()) })
+               val emailSent = AtomicBoolean(false)
+               val errorEmailAction = createEmailAction { emailSent.set(true) }
+               val reactionRunner = ReactionRunner(failingReaction, reactionState, errorEmailAction)
+               reactionRunner.run()
+               assertThat(emailSent.get(), equalTo(false))
+       }
+
+       @Test
+       fun `reaction runner saves state when unsuccessful and last state does not exist`() {
+               val savedState = AtomicReference<State>()
+               val reactionState = createReactionState(saveState = { _, state -> savedState.set(state) })
+               val reactionRunner = ReactionRunner(failingReaction, reactionState, nullEmailAction)
+               reactionRunner.run()
+               assertThat(savedState.get(), isState(success = equalTo(false), failCount = equalTo(1)))
+       }
+
+       @Test
+       fun `reaction runner saves state when unsuccessful and last state is failed`() {
+               val savedState = AtomicReference<State>()
+               val failedState = FailedState().apply { setFailCount(12) }
+               val reactionState = createReactionState(lastState = { of<State>(failedState) }, saveState = { _, state -> savedState.set(state) })
+               val reactionRunner = ReactionRunner(failingReaction, reactionState, nullEmailAction)
+               reactionRunner.run()
+               assertThat(savedState.get(), isState(success = equalTo(false), failCount = equalTo(13)))
+       }
+
+       private fun createReactionState(lastState: (reactionName: String) -> Optional<State> = { empty<State>() }, lastSuccessfulState: (reactionName: String) -> Optional<State> = { empty<State>() }, saveState: (reactionName: String, State) -> Unit = { reactionName, state -> }) =
+               ReactionState(object : StateManager(StateDirectory.of("")) {
+                       override fun loadLastState(reactionName: String) = lastState(reactionName)
+                       override fun loadLastSuccessfulState(reactionName: String) = lastSuccessfulState(reactionName)
+                       override fun saveState(reactionName: String, state: State) = saveState(reactionName, state)
+               }, "Test Reaction")
+
+       private fun createEmailAction(action: () -> Unit = {}) = object : EmailAction("test.test", "sender@test.test", "recipient@test.test") {
+               override fun execute(output: Output?) = action()
+       }
+
+       private val reactionState = createReactionState()
+       private val failingReaction = object : Reaction("Test", null, null, null) {
+               override fun query() = Query { FailedState() }
+       }
+       private val nullEmailAction = createEmailAction { }
+
+}
+
+private fun isState(success: Matcher<Boolean> = equalTo(true), failCount: Matcher<Int> = equalTo(0)) = object : TypeSafeDiagnosingMatcher<State>(State::class.java) {
+       override fun matchesSafely(item: State, mismatchDescription: Description): Boolean {
+               if (!success.matches(item.success())) {
+                       mismatchDescription.appendText("success is ").appendValue(item.success())
+                       return false
+               }
+               if (!failCount.matches(item.failCount())) {
+                       mismatchDescription.appendText("failCount is ").appendValue(item.failCount())
+                       return false
+               }
+               return true
+       }
+
+       override fun describeTo(description: Description) {
+               description.appendText("state with success ").appendValue(success)
+                       .appendText(" and failCount ").appendValue(failCount)
+       }
+}