aboutsummaryrefslogtreecommitdiff
path: root/app/src/test/kotlin/com/pitchedapps
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/test/kotlin/com/pitchedapps')
-rw-r--r--app/src/test/kotlin/com/pitchedapps/frost/MiscTest.kt13
-rw-r--r--app/src/test/kotlin/com/pitchedapps/frost/rx/FlyweightTest.kt108
-rw-r--r--app/src/test/kotlin/com/pitchedapps/frost/utils/CoroutineTest.kt194
3 files changed, 302 insertions, 13 deletions
diff --git a/app/src/test/kotlin/com/pitchedapps/frost/MiscTest.kt b/app/src/test/kotlin/com/pitchedapps/frost/MiscTest.kt
index ce125298..20610b2a 100644
--- a/app/src/test/kotlin/com/pitchedapps/frost/MiscTest.kt
+++ b/app/src/test/kotlin/com/pitchedapps/frost/MiscTest.kt
@@ -16,9 +16,7 @@
*/
package com.pitchedapps.frost
-import com.pitchedapps.frost.facebook.requests.call
import com.pitchedapps.frost.facebook.requests.zip
-import okhttp3.Request
import org.junit.Test
import kotlin.test.assertTrue
@@ -47,15 +45,4 @@ class MiscTest {
"zip did not seem to work on different threads"
)
}
-
- @Test
- fun a() {
- val s = Request.Builder()
- .url("https://www.allanwang.ca/ecse429/magenta.png")
- .get()
- .call().execute().body()!!.string()
- "�PNG\n\u001A\nIDA�c����?\u0000\u0006�\u0002��p�\u0000\u0000\u0000\u0000IEND�B`�"
- println("Hello")
- println(s)
- }
}
diff --git a/app/src/test/kotlin/com/pitchedapps/frost/rx/FlyweightTest.kt b/app/src/test/kotlin/com/pitchedapps/frost/rx/FlyweightTest.kt
new file mode 100644
index 00000000..834163bd
--- /dev/null
+++ b/app/src/test/kotlin/com/pitchedapps/frost/rx/FlyweightTest.kt
@@ -0,0 +1,108 @@
+package com.pitchedapps.frost.rx
+
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.async
+import kotlinx.coroutines.runBlocking
+import org.junit.Rule
+import org.junit.rules.Timeout
+import java.util.concurrent.atomic.AtomicInteger
+import kotlin.test.BeforeTest
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+import kotlin.test.fail
+
+class FlyweightTest {
+
+ @get:Rule
+ val globalTimeout: Timeout = Timeout.seconds(5)
+
+ lateinit var flyweight: Flyweight<Int, Int>
+
+ lateinit var callCount: AtomicInteger
+
+ private val LONG_RUNNING_KEY = -78
+
+ @BeforeTest
+ fun before() {
+ callCount = AtomicInteger(0)
+ flyweight = Flyweight(GlobalScope, 100, 200L) {
+ callCount.incrementAndGet()
+ when (it) {
+ LONG_RUNNING_KEY -> Thread.sleep(100000)
+ else -> Thread.sleep(100)
+ }
+ it * 2
+ }
+ }
+
+ @Test
+ fun basic() {
+ assertEquals(2, runBlocking { flyweight.fetch(1) }, "Invalid result")
+ assertEquals(1, callCount.get(), "1 call expected")
+ }
+
+ @Test
+ fun multipleWithOneKey() {
+ val results: List<Int> = runBlocking {
+ (0..1000).map {
+ flyweight.scope.async {
+ flyweight.fetch(1)
+ }
+ }.map { it.await() }
+ }
+ assertEquals(1, callCount.get(), "1 call expected")
+ assertEquals(1001, results.size, "Incorrect number of results returned")
+ assertTrue(results.all { it == 2 }, "Result should all be 2")
+ }
+
+ @Test
+ fun consecutiveReuse() {
+ runBlocking {
+ flyweight.fetch(1)
+ assertEquals(1, callCount.get(), "1 call expected")
+ flyweight.fetch(1)
+ assertEquals(1, callCount.get(), "Reuse expected")
+ Thread.sleep(300)
+ flyweight.fetch(1)
+ assertEquals(2, callCount.get(), "Refetch expected")
+ }
+ }
+
+ @Test
+ fun invalidate() {
+ runBlocking {
+ flyweight.fetch(1)
+ assertEquals(1, callCount.get(), "1 call expected")
+ flyweight.invalidate(1)
+ flyweight.fetch(1)
+ assertEquals(2, callCount.get(), "New call expected")
+ }
+ }
+
+ @Test
+ fun destroy() {
+ runBlocking {
+ val longRunningResult = async { flyweight.fetch(LONG_RUNNING_KEY) }
+ flyweight.fetch(1)
+ flyweight.cancel()
+ try {
+ flyweight.fetch(1)
+ fail("Flyweight should not be fulfilled after it is destroyed")
+ } catch (e: Exception) {
+ assertEquals("Flyweight is not active", e.message, "Incorrect error found on fetch after destruction")
+ }
+ try {
+ longRunningResult.await()
+ fail("Flyweight should have cancelled previously running requests")
+ } catch (e: Exception) {
+ assertEquals(
+ "Flyweight cancelled",
+ e.message,
+ "Incorrect error found on fetch cancelled by destruction"
+ )
+ }
+ println("Done")
+ }
+ }
+} \ No newline at end of file
diff --git a/app/src/test/kotlin/com/pitchedapps/frost/utils/CoroutineTest.kt b/app/src/test/kotlin/com/pitchedapps/frost/utils/CoroutineTest.kt
new file mode 100644
index 00000000..f930e529
--- /dev/null
+++ b/app/src/test/kotlin/com/pitchedapps/frost/utils/CoroutineTest.kt
@@ -0,0 +1,194 @@
+package com.pitchedapps.frost.utils
+
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.async
+import kotlinx.coroutines.channels.BroadcastChannel
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.channels.ReceiveChannel
+import kotlinx.coroutines.channels.count
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.joinAll
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
+import java.util.concurrent.Executors
+import kotlin.coroutines.EmptyCoroutineContext
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+/**
+ * Collection of tests around coroutines
+ */
+@UseExperimental(ExperimentalCoroutinesApi::class)
+class CoroutineTest {
+
+ /**
+ * Hooks onto the refresh channel for one true -> false cycle.
+ * Returns the list of event ids that were emitted
+ */
+ private suspend fun transition(channel: ReceiveChannel<Pair<Boolean, Int>>): List<Pair<Boolean, Int>> {
+ var refreshed = false
+ return listen(channel) { (refreshing, _) ->
+ if (refreshed && !refreshing)
+ return@listen true
+ if (refreshing)
+ refreshed = true
+ return@listen false
+ }
+ }
+
+ private suspend fun <T> listen(channel: ReceiveChannel<T>, shouldEnd: suspend (T) -> Boolean = { false }): List<T> =
+ withContext(Dispatchers.IO) {
+ val data = mutableListOf<T>()
+ for (c in channel) {
+ data.add(c)
+ if (shouldEnd(c)) break
+ }
+ channel.cancel()
+ return@withContext data
+ }
+
+ /**
+ * When refreshing, we have a temporary subscriber that hooks onto a single cycle.
+ * The refresh channel only contains booleans, but for the sake of identification,
+ * each boolean will have a unique integer attached.
+ *
+ * Things to note:
+ * Subscription should be opened outside of async, since we don't want to miss any events.
+ */
+ @Test
+ fun refreshSubscriptions() {
+ val refreshChannel = BroadcastChannel<Pair<Boolean, Int>>(100)
+ runBlocking {
+ // Listen to all events
+ val fullReceiver = refreshChannel.openSubscription()
+ val fullDeferred = async { listen(fullReceiver) }
+
+ refreshChannel.send(true to 1)
+ refreshChannel.send(false to 2)
+ refreshChannel.send(true to 3)
+
+ val partialReceiver = refreshChannel.openSubscription()
+ val partialDeferred = async { transition(partialReceiver) }
+ refreshChannel.send(false to 4)
+ refreshChannel.send(true to 5)
+ refreshChannel.send(false to 6)
+ refreshChannel.send(true to 7)
+ refreshChannel.close()
+ val fullStream = fullDeferred.await()
+ val partialStream = partialDeferred.await()
+
+ assertEquals(
+ 7,
+ fullStream.size,
+ "Full stream should contain all events"
+ )
+ assertEquals(
+ listOf(false to 4, true to 5, false to 6),
+ partialStream,
+ "Partial stream should include up until first true false pair"
+ )
+ }
+ }
+
+ /**
+ * Sanity check to ensure that contexts are being honoured
+ */
+ @Test
+ fun contextSwitching() {
+ val mainTag = "main-test"
+ val mainDispatcher = Executors.newSingleThreadExecutor { r ->
+ Thread(r, mainTag)
+ }.asCoroutineDispatcher()
+
+ val channel = BroadcastChannel<String>(100)
+
+ runBlocking(Dispatchers.IO) {
+ val receiver1 = channel.openSubscription()
+ val receiver2 = channel.openSubscription()
+ launch(mainDispatcher) {
+ for (thread in receiver1) {
+ assertTrue(
+ Thread.currentThread().name.startsWith(mainTag),
+ "Channel should be received in main thread"
+ )
+ assertFalse(
+ thread.startsWith(mainTag),
+ "Channel execution should not be in main thread"
+ )
+ }
+ }
+ listOf(EmptyCoroutineContext, Dispatchers.IO, Dispatchers.Default, Dispatchers.IO).map {
+ async(it) { channel.send(Thread.currentThread().name) }
+ }.joinAll()
+ channel.close()
+ assertEquals(4, receiver2.count(), "Not all events received")
+ }
+ }
+
+ /**
+ * Not a true throttle, but for things like fetching header badges, we want to avoid simultaneous fetches.
+ * As a result, I want to test that the usage of offer along with a rendezvous channel will work as I expect.
+ * Events should be consumed when there is no pending consumer on previous elements.
+ */
+ @Test
+ fun throttledChannel() {
+ val channel = Channel<Int>(Channel.RENDEZVOUS)
+ runBlocking {
+ val deferred = async {
+ listen(channel) {
+ // Throttle consumer
+ delay(10)
+ return@listen false
+ }
+ }
+ (0..100).forEach {
+ channel.offer(it)
+ delay(1)
+ }
+ channel.close()
+ val received = deferred.await()
+ assertTrue(
+ received.size < 20,
+ "Received data should be throttled; expected that around 1/10th of all events are consumed"
+ )
+ println(received)
+ }
+ }
+
+ @Test
+ fun uniqueOnly() {
+ val channel = BroadcastChannel<Int>(100)
+ runBlocking {
+ val fullReceiver = channel.openSubscription()
+ val uniqueReceiver = channel.openSubscription().uniqueOnly(this)
+
+ val fullDeferred = async { listen(fullReceiver) }
+ val uniqueDeferred = async { listen(uniqueReceiver) }
+
+ listOf(0, 1, 2, 3, 3, 3, 4, 3, 5, 5, 1).forEach {
+ channel.offer(it)
+ }
+ channel.close()
+
+ val fullData = fullDeferred.await()
+ val uniqueData = uniqueDeferred.await()
+
+ assertEquals(
+ listOf(0, 1, 2, 3, 3, 3, 4, 3, 5, 5, 1),
+ fullData,
+ "Full receiver should get all channel events"
+ )
+ assertEquals(
+ listOf(0, 1, 2, 3, 4, 3, 5, 1),
+ uniqueData,
+ "Unique receiver should not have two consecutive events that are equal"
+ )
+
+ }
+ }
+} \ No newline at end of file