diff --git a/parsely/src/main/java/com/parsely/parselyandroid/LocalStorageRepository.kt b/parsely/src/main/java/com/parsely/parselyandroid/LocalStorageRepository.kt index ce785439..63c695ac 100644 --- a/parsely/src/main/java/com/parsely/parselyandroid/LocalStorageRepository.kt +++ b/parsely/src/main/java/com/parsely/parselyandroid/LocalStorageRepository.kt @@ -6,7 +6,7 @@ import java.io.FileNotFoundException import java.io.ObjectInputStream import java.io.ObjectOutputStream -internal class LocalStorageRepository(private val context: Context) { +internal open class LocalStorageRepository(private val context: Context) { /** * Persist an object to storage. * @@ -33,7 +33,7 @@ internal class LocalStorageRepository(private val context: Context) { persistObject(ArrayList>()) } - val storedQueue: ArrayList?> + open val storedQueue: ArrayList?> /** * Get the stored event queue from persistent storage. * @@ -65,7 +65,7 @@ internal class LocalStorageRepository(private val context: Context) { /** * Delete an event from the stored queue. */ - fun expelStoredEvent() { + open fun expelStoredEvent() { val storedQueue = storedQueue storedQueue.removeAt(0) } @@ -74,7 +74,7 @@ internal class LocalStorageRepository(private val context: Context) { * Save the event queue to persistent storage. */ @Synchronized - fun persistQueue(inMemoryQueue: List?>) { + open fun persistQueue(inMemoryQueue: List?>) { ParselyTracker.PLog("Persisting event queue") val storedQueue = storedQueue val hs = HashSet?>() diff --git a/parsely/src/main/java/com/parsely/parselyandroid/ParselyTracker.java b/parsely/src/main/java/com/parsely/parselyandroid/ParselyTracker.java index 23001bfe..3c053d08 100644 --- a/parsely/src/main/java/com/parsely/parselyandroid/ParselyTracker.java +++ b/parsely/src/main/java/com/parsely/parselyandroid/ParselyTracker.java @@ -35,6 +35,7 @@ import java.util.Formatter; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Timer; import java.util.UUID; @@ -52,7 +53,7 @@ public class ParselyTracker { @SuppressWarnings("StringOperationCanBeSimplified") // private static final String ROOT_URL = "http://10.0.2.2:5001/".intern(); // emulator localhost private static final String ROOT_URL = "https://p1.parsely.com/".intern(); - protected ArrayList> eventQueue; + private final ArrayList> eventQueue; private boolean isDebug; private final Context context; private final Timer timer; @@ -97,6 +98,10 @@ protected ParselyTracker(String siteId, int flushInterval, Context c) { ); } + List> getInMemoryQueue() { + return eventQueue; + } + /** * Singleton instance accessor. Note: This must be called after {@link #sharedInstance(String, Context)} * diff --git a/parsely/src/main/java/com/parsely/parselyandroid/QueueManager.java b/parsely/src/main/java/com/parsely/parselyandroid/QueueManager.java index 7a9a38ee..c05d1fe2 100644 --- a/parsely/src/main/java/com/parsely/parselyandroid/QueueManager.java +++ b/parsely/src/main/java/com/parsely/parselyandroid/QueueManager.java @@ -5,8 +5,8 @@ import androidx.annotation.NonNull; class QueueManager extends AsyncTask { - private static final int QUEUE_SIZE_LIMIT = 50; - private static final int STORAGE_SIZE_LIMIT = 100; + static final int QUEUE_SIZE_LIMIT = 50; + static final int STORAGE_SIZE_LIMIT = 100; @NonNull private final ParselyTracker parselyTracker; @@ -24,10 +24,10 @@ public QueueManager( @Override protected Void doInBackground(Void... params) { // if event queue is too big, push to persisted storage - if (parselyTracker.eventQueue.size() > QUEUE_SIZE_LIMIT) { + if (parselyTracker.getInMemoryQueue().size() > QUEUE_SIZE_LIMIT) { ParselyTracker.PLog("Queue size exceeded, expelling oldest event to persistent memory"); - localStorageRepository.persistQueue(parselyTracker.eventQueue); - parselyTracker.eventQueue.remove(0); + localStorageRepository.persistQueue(parselyTracker.getInMemoryQueue()); + parselyTracker.getInMemoryQueue().remove(0); // if persisted storage is too big, expel one if (parselyTracker.storedEventsCount() > STORAGE_SIZE_LIMIT) { localStorageRepository.expelStoredEvent(); diff --git a/parsely/src/test/java/com/parsely/parselyandroid/QueueManagerTest.kt b/parsely/src/test/java/com/parsely/parselyandroid/QueueManagerTest.kt new file mode 100644 index 00000000..d73bbd3f --- /dev/null +++ b/parsely/src/test/java/com/parsely/parselyandroid/QueueManagerTest.kt @@ -0,0 +1,109 @@ +package com.parsely.parselyandroid + +import androidx.test.core.app.ApplicationProvider +import com.parsely.parselyandroid.QueueManager.QUEUE_SIZE_LIMIT +import com.parsely.parselyandroid.QueueManager.STORAGE_SIZE_LIMIT +import org.assertj.core.api.Assertions.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.LooperMode +import org.robolectric.shadows.ShadowLooper.shadowMainLooper + +@RunWith(RobolectricTestRunner::class) +@LooperMode(LooperMode.Mode.PAUSED) +internal class QueueManagerTest { + + private lateinit var sut: QueueManager + + private val tracker = FakeTracker() + private val repository = FakeLocalRepository() + + @Before + fun setUp() { + sut = QueueManager(tracker, repository) + } + + @Test + fun `given the queue is smaller than any threshold, when querying flush manager, do nothing`() { + // given + val initialInMemoryQueue = listOf(mapOf("test" to "test")) + tracker.applyFakeQueue(initialInMemoryQueue) + + // when + sut.execute().get() + shadowMainLooper().idle(); + + // then + assertThat(tracker.inMemoryQueue).isEqualTo(initialInMemoryQueue) + assertThat(repository.storedQueue).isEmpty() + } + + @Test + fun `given the in-memory queue is above the in-memory limit, when querying flush manager, then save queue to local storage and remove first event`() { + // given + val initialInMemoryQueue = (1..QUEUE_SIZE_LIMIT + 1).map { mapOf("test" to it) } + tracker.applyFakeQueue(initialInMemoryQueue) + + // when + sut.execute().get() + shadowMainLooper().idle(); + + // then + assertThat(repository.storedQueue).isEqualTo(initialInMemoryQueue) + assertThat(tracker.inMemoryQueue).hasSize(QUEUE_SIZE_LIMIT) + } + + @Test + fun `given the in-memory queue is above the in-memory limit and stored events queue is above stored-queue limit, when querying flush manager, then expel the last event from local storage`() { + // given + val initialInMemoryQueue = (1..QUEUE_SIZE_LIMIT + 1).map { mapOf("in memory" to it) } + tracker.applyFakeQueue(initialInMemoryQueue) + val initialStoredQueue = (1..STORAGE_SIZE_LIMIT + 1).map { mapOf("storage" to it) } + repository.persistQueue(initialStoredQueue) + + // when + sut.execute().get() + shadowMainLooper().idle(); + + // then + assertThat(repository.wasEventExpelled).isTrue + } + + inner class FakeTracker : ParselyTracker( + "siteId", 10, ApplicationProvider.getApplicationContext() + ) { + + private var fakeQueue: List> = emptyList() + + internal override fun getInMemoryQueue(): List> = fakeQueue + + fun applyFakeQueue(fakeQueue: List>) { + this.fakeQueue = fakeQueue.toList() + } + + override fun storedEventsCount(): Int { + return repository.storedQueue.size + } + } + + class FakeLocalRepository : + LocalStorageRepository(ApplicationProvider.getApplicationContext()) { + + private var localFileQueue = emptyList?>() + var wasEventExpelled = false + + override fun persistQueue(inMemoryQueue: List?>) { + this.localFileQueue += inMemoryQueue + } + + override val storedQueue: ArrayList?> + get() = ArrayList(localFileQueue) + + + override fun expelStoredEvent() { + wasEventExpelled = true + } + } +}