diff options
Diffstat (limited to 'app/src/test/kotlin')
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 |