From 8ba1240d5e15059e7469b46163c6f640dedefabb Mon Sep 17 00:00:00 2001 From: =?utf8?q?David=20=E2=80=98Bombe=E2=80=99=20Roden?= Date: Sat, 3 Mar 2018 17:31:50 +0100 Subject: [PATCH] Add processor for updated Sones --- .../sone/core/UpdatedSoneProcessor.kt | 68 ++++++++++ .../sone/core/UpdatedSoneProcessorTest.kt | 151 +++++++++++++++++++++ 2 files changed, 219 insertions(+) create mode 100644 src/main/kotlin/net/pterodactylus/sone/core/UpdatedSoneProcessor.kt create mode 100644 src/test/kotlin/net/pterodactylus/sone/core/UpdatedSoneProcessorTest.kt diff --git a/src/main/kotlin/net/pterodactylus/sone/core/UpdatedSoneProcessor.kt b/src/main/kotlin/net/pterodactylus/sone/core/UpdatedSoneProcessor.kt new file mode 100644 index 0000000..17948c5 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/core/UpdatedSoneProcessor.kt @@ -0,0 +1,68 @@ +package net.pterodactylus.sone.core + +import com.google.common.eventbus.EventBus +import com.google.inject.ImplementedBy +import net.pterodactylus.sone.core.event.NewPostFoundEvent +import net.pterodactylus.sone.core.event.NewPostReplyFoundEvent +import net.pterodactylus.sone.core.event.PostRemovedEvent +import net.pterodactylus.sone.core.event.PostReplyRemovedEvent +import net.pterodactylus.sone.data.Sone +import net.pterodactylus.sone.data.Sone.SoneStatus +import net.pterodactylus.sone.database.Database +import net.pterodactylus.sone.utils.ifFalse +import net.pterodactylus.util.logging.Logging +import javax.inject.Inject + +/** + * An `UpdatedSoneProcessor` is called to process a [Sone] after it has been + * downloaded from Freenet. + */ +@ImplementedBy(DefaultUpdateSoneProcessor::class) +interface UpdatedSoneProcessor { + + fun updateSone(sone: Sone) + +} + +abstract class BasicUpdateSoneProcessor(private val database: Database, private val eventBus: EventBus) : + UpdatedSoneProcessor { + + private val logger = Logging.getLogger(UpdatedSoneProcessor::javaClass.name)!! + + override fun updateSone(sone: Sone) { + val storedSone = database.getSone(sone.id) ?: return + if (!soneCanBeUpdated(storedSone, sone)) { + logger.fine { "Downloaded Sone $sone can not update stored Sone $storedSone." } + return + } + collectEventsForChanges(storedSone, sone) + .also { database.storeSone(sone) } + .forEach(eventBus::post) + sone.options = storedSone.options + sone.isKnown = storedSone.isKnown + sone.status = if (sone.time != 0L) SoneStatus.idle else SoneStatus.unknown + } + + protected abstract fun soneCanBeUpdated(storedSone: Sone, newSone: Sone): Boolean + + private val Sone.followingTime get() = database.getFollowingTime(id) ?: 0 + + private fun collectEventsForChanges(oldSone: Sone, newSone: Sone): List = + SoneChangeCollector(oldSone) + .onNewPost { post -> if (post.time <= newSone.followingTime) post.isKnown = true } + .newPostEvent { post -> post.isKnown.ifFalse { NewPostFoundEvent(post) } } + .removedPostEvent { PostRemovedEvent(it) } + .onNewPostReply { postReply -> if (postReply.time <= newSone.followingTime) postReply.isKnown = true } + .newPostReplyEvent { postReply -> postReply.isKnown.ifFalse { NewPostReplyFoundEvent(postReply) } } + .onRemovedPostReply { PostReplyRemovedEvent(it) } + .detectChanges(newSone) + +} + +class DefaultUpdateSoneProcessor @Inject constructor(database: Database, eventBus: EventBus) : + BasicUpdateSoneProcessor(database, eventBus) { + + override fun soneCanBeUpdated(storedSone: Sone, newSone: Sone) = + newSone.time > storedSone.time + +} diff --git a/src/test/kotlin/net/pterodactylus/sone/core/UpdatedSoneProcessorTest.kt b/src/test/kotlin/net/pterodactylus/sone/core/UpdatedSoneProcessorTest.kt new file mode 100644 index 0000000..e7afc74 --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/core/UpdatedSoneProcessorTest.kt @@ -0,0 +1,151 @@ +package net.pterodactylus.sone.core + +import com.google.common.eventbus.EventBus +import net.pterodactylus.sone.core.event.NewPostFoundEvent +import net.pterodactylus.sone.core.event.NewPostReplyFoundEvent +import net.pterodactylus.sone.core.event.PostRemovedEvent +import net.pterodactylus.sone.core.event.PostReplyRemovedEvent +import net.pterodactylus.sone.data.Post +import net.pterodactylus.sone.data.PostReply +import net.pterodactylus.sone.data.Sone +import net.pterodactylus.sone.database.Database +import net.pterodactylus.sone.test.argumentCaptor +import net.pterodactylus.sone.test.getInstance +import net.pterodactylus.sone.test.isProvidedByMock +import net.pterodactylus.sone.test.mock +import net.pterodactylus.sone.test.whenever +import net.pterodactylus.sone.web.baseInjector +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.contains +import org.hamcrest.Matchers.containsInAnyOrder +import org.hamcrest.Matchers.not +import org.hamcrest.Matchers.notNullValue +import org.junit.Before +import org.junit.Test +import org.mockito.Mockito.any +import org.mockito.Mockito.atLeastOnce +import org.mockito.Mockito.never +import org.mockito.Mockito.times +import org.mockito.Mockito.verify + +/** + * Unit test for all [UpdatedSoneProcessor] implementations. + */ +class UpdatedSoneProcessorTest { + + private val database = mock() + private val eventBus = mock() + private val updatedSoneProcessor = DefaultUpdateSoneProcessor(database, eventBus) + private val storedSone = mock() + private val newSone = mock() + private val posts = listOf(mock(), mock(), mock()) + private val postReplies = listOf(mock(), mock(), mock()) + + private val events = argumentCaptor() + + @Before + fun setupPostsAndReplies() { + posts.forEachIndexed { index, post -> whenever(post.time).thenReturn((index + 1) * 1000L + 100) } + postReplies.forEachIndexed { index, postReply -> whenever(postReply.time).thenReturn((index + 1) * 1000L + 200) } + } + + @Before + fun setupSones() { + whenever(storedSone.time).thenReturn(1000L) + whenever(storedSone.posts).thenReturn(posts.slice(0..1)) + whenever(storedSone.replies).thenReturn(postReplies.slice(0..1).toSet()) + whenever(newSone.id).thenReturn("sone") + whenever(newSone.time).thenReturn(2000L) + whenever(newSone.posts).thenReturn(posts.slice(1..2)) + whenever(newSone.replies).thenReturn(postReplies.slice(1..2).toSet()) + } + + @Before + fun setupDatabase() { + whenever(database.getSone("sone")).thenReturn(storedSone) + whenever(database.getFollowingTime("sone")).thenReturn(500L) + } + + @Test + fun `updated Sone processor emits no event if no stored sone exists`() { + whenever(database.getSone("sone")).thenReturn(null) + updatedSoneProcessor.updateSone(newSone) + verify(eventBus, never()).post(any()) + } + + @Test + fun `updated Sone processor emits no event if new Sone is older than stored Sone`() { + whenever(newSone.time).thenReturn(500L) + updatedSoneProcessor.updateSone(newSone) + verify(eventBus, never()).post(any()) + } + + @Test + fun `updated Sone processor emits correct events when new Sone is newer`() { + updatedSoneProcessor.updateSone(newSone) + verify(eventBus, times(4)).post(events.capture()) + assertThat(events.allValues, containsInAnyOrder( + NewPostFoundEvent(posts[2]), + PostRemovedEvent(posts[0]), + NewPostReplyFoundEvent(postReplies[2]), + PostReplyRemovedEvent(postReplies[0]) + )) + } + + @Test + fun `updated Sone processor does not mark new post as known if sone was not followed after post`() { + updatedSoneProcessor.updateSone(newSone) + verify(posts[2], never()).isKnown = true + } + + @Test + fun `updated Sone processor does not mark new posts as known if Sone is not followed`() { + whenever(database.getFollowingTime("sone")).thenReturn(null) + updatedSoneProcessor.updateSone(newSone) + posts.forEach { verify(it, never()).isKnown = true } + } + + @Test + fun `updated Sone processor marks new post as known if sone was followed after post`() { + whenever(database.getFollowingTime("sone")).thenReturn(3500L) + updatedSoneProcessor.updateSone(newSone) + verify(posts[2]).isKnown = true + } + + @Test + fun `updated Sone processor does not emit event for post if it is already known`() { + whenever(posts[2].isKnown).thenReturn(true) + updatedSoneProcessor.updateSone(newSone) + verify(eventBus, atLeastOnce()).post(events.capture()) + assertThat(events.allValues, not(contains(NewPostFoundEvent(posts[2])))) + } + + @Test + fun `updated Sone processor does not mark new reply as known if sone was not followed after reply`() { + updatedSoneProcessor.updateSone(newSone) + verify(postReplies[2], never()).isKnown = true + } + + @Test + fun `updated Sone processor marks new reply as known if sone was followed after reply`() { + whenever(database.getFollowingTime("sone")).thenReturn(3500L) + updatedSoneProcessor.updateSone(newSone) + verify(postReplies[2]).isKnown = true + } + + @Test + fun `updated Sone processor does not emit event for reply if it is already known`() { + whenever(postReplies[2].isKnown).thenReturn(true) + updatedSoneProcessor.updateSone(newSone) + verify(eventBus, atLeastOnce()).post(events.capture()) + assertThat(events.allValues, not(contains(NewPostReplyFoundEvent(postReplies[2])))) + } + + @Test + fun `default updated Sone processor can be created by dependency injection`() { + assertThat(baseInjector.createChildInjector( + Database::class.isProvidedByMock() + ).getInstance(), notNullValue()) + } + +} -- 2.7.4