1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
|
package com.pitchedapps.frost.views
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExecutorCoroutineDispatcher
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.async
import kotlinx.coroutines.channels.BroadcastChannel
import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.util.concurrent.Executors
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertEquals
/**
* Collection of tests around the view thread logic
*/
@UseExperimental(ExperimentalCoroutinesApi::class)
class FrostContentViewAsyncTest {
/**
* Single threaded dispatcher with thread name "main"
* Mimics the usage of Android's main dispatcher
*/
private lateinit var mainDispatcher: ExecutorCoroutineDispatcher
@BeforeTest
fun before() {
mainDispatcher = Executors.newSingleThreadExecutor { r ->
Thread(r, "main")
}.asCoroutineDispatcher()
}
@AfterTest
fun after() {
mainDispatcher.close()
}
/**
* 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: (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"
)
}
}
}
|