1 /*
<lambda>null2 * Copyright (C) 2025 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package com.android.systemui.kairos
18
19 import com.android.systemui.kairos.util.MapPatch
20 import com.android.systemui.kairos.util.These
21 import com.android.systemui.kairos.util.maybeOf
22 import com.android.systemui.kairos.util.toMaybe
23 import kotlin.time.Duration.Companion.seconds
24 import kotlinx.coroutines.ExperimentalCoroutinesApi
25 import kotlinx.coroutines.launch
26 import kotlinx.coroutines.test.TestDispatcher
27 import kotlinx.coroutines.test.UnconfinedTestDispatcher
28 import kotlinx.coroutines.test.runTest
29 import org.junit.Assert
30 import org.junit.Assert.assertTrue
31 import org.junit.Test
32
33 @OptIn(ExperimentalCoroutinesApi::class)
34 class KairosSamples {
35
36 @Test fun test_mapMaybe() = runSample { mapMaybe() }
37
38 fun BuildScope.mapMaybe() {
39 val emitter = MutableEvents<String>()
40 val ints = emitter.mapMaybe { it.toIntOrNull().toMaybe() }
41
42 var observedInput: String? = null
43 emitter.observe { observedInput = it }
44
45 var observedInt: Int? = null
46 ints.observe { observedInt = it }
47
48 launchEffect {
49 // parse succeeds
50 emitter.emit("6")
51 assertEquals(observedInput, "6")
52 assertEquals(observedInt, 6)
53
54 // parse fails
55 emitter.emit("foo")
56 assertEquals(observedInput, "foo")
57 assertEquals(observedInt, 6)
58
59 // parse succeeds
60 emitter.emit("500")
61 assertEquals(observedInput, "500")
62 assertEquals(observedInt, 500)
63 }
64 }
65
66 @Test fun test_mapCheap() = runSample { mapCheap() }
67
68 fun BuildScope.mapCheap() {
69 val emitter = MutableEvents<Int>()
70
71 var invocationCount = 0
72 val squared =
73 emitter.mapCheap {
74 invocationCount++
75 it * it
76 }
77
78 var observedSquare: Int? = null
79 squared.observe { observedSquare = it }
80
81 launchEffect {
82 emitter.emit(10)
83 assertTrue(invocationCount >= 1)
84 assertEquals(observedSquare, 100)
85
86 emitter.emit(2)
87 assertTrue(invocationCount >= 2)
88 assertEquals(observedSquare, 4)
89 }
90 }
91
92 @Test fun test_mapEvents() = runSample { mapEvents() }
93
94 fun BuildScope.mapEvents() {
95 val emitter = MutableEvents<Int>()
96
97 val squared = emitter.map { it * it }
98
99 var observedSquare: Int? = null
100 squared.observe { observedSquare = it }
101
102 launchEffect {
103 emitter.emit(10)
104 assertEquals(observedSquare, 100)
105
106 emitter.emit(2)
107 assertEquals(observedSquare, 4)
108 }
109 }
110
111 @Test fun test_eventsLoop() = runSample { eventsLoop() }
112
113 fun BuildScope.eventsLoop() {
114 val emitter = MutableEvents<Unit>()
115 var newCount: Events<Int> by EventsLoop()
116 val count = newCount.holdState(0)
117 newCount = emitter.map { count.sample() + 1 }
118
119 var observedCount = 0
120 count.observe { observedCount = it }
121
122 launchEffect {
123 emitter.emit(Unit)
124 assertEquals(observedCount, expected = 1)
125
126 emitter.emit(Unit)
127 assertEquals(observedCount, expected = 2)
128 }
129 }
130
131 @Test fun test_stateLoop() = runSample { stateLoop() }
132
133 fun BuildScope.stateLoop() {
134 val emitter = MutableEvents<Unit>()
135 var count: State<Int> by StateLoop()
136 count = emitter.map { count.sample() + 1 }.holdState(0)
137
138 var observedCount = 0
139 count.observe { observedCount = it }
140
141 launchEffect {
142 emitter.emit(Unit)
143 assertEquals(observedCount, expected = 1)
144
145 emitter.emit(Unit)
146 assertEquals(observedCount, expected = 2)
147 }
148 }
149
150 @Test fun test_changes() = runSample { changes() }
151
152 fun BuildScope.changes() {
153 val emitter = MutableEvents<Int>()
154 val state = emitter.holdState(0)
155
156 var numEmissions = 0
157 emitter.observe { numEmissions++ }
158
159 var observedState = 0
160 var numChangeEmissions = 0
161 state.changes.observe {
162 observedState = it
163 numChangeEmissions++
164 }
165
166 launchEffect {
167 emitter.emit(0)
168 assertEquals(numEmissions, expected = 1)
169 assertEquals(numChangeEmissions, expected = 0)
170 assertEquals(observedState, expected = 0)
171
172 emitter.emit(5)
173 assertEquals(numEmissions, expected = 2)
174 assertEquals(numChangeEmissions, expected = 1)
175 assertEquals(observedState, expected = 5)
176
177 emitter.emit(3)
178 assertEquals(numEmissions, expected = 3)
179 assertEquals(numChangeEmissions, expected = 2)
180 assertEquals(observedState, expected = 3)
181
182 emitter.emit(3)
183 assertEquals(numEmissions, expected = 4)
184 assertEquals(numChangeEmissions, expected = 2)
185 assertEquals(observedState, expected = 3)
186
187 emitter.emit(5)
188 assertEquals(numEmissions, expected = 5)
189 assertEquals(numChangeEmissions, expected = 3)
190 assertEquals(observedState, expected = 5)
191 }
192 }
193
194 @Test fun test_partitionThese() = runSample { partitionThese() }
195
196 fun BuildScope.partitionThese() {
197 val emitter = MutableEvents<These<Int, String>>()
198 val (lefts, rights) = emitter.partitionThese()
199
200 var observedLeft: Int? = null
201 lefts.observe { observedLeft = it }
202
203 var observedRight: String? = null
204 rights.observe { observedRight = it }
205
206 launchEffect {
207 emitter.emit(These.first(10))
208 assertEquals(observedLeft, 10)
209 assertEquals(observedRight, null)
210
211 emitter.emit(These.both(2, "foo"))
212 assertEquals(observedLeft, 2)
213 assertEquals(observedRight, "foo")
214
215 emitter.emit(These.second("bar"))
216 assertEquals(observedLeft, 2)
217 assertEquals(observedRight, "bar")
218 }
219 }
220
221 @Test fun test_merge() = runSample { merge() }
222
223 fun BuildScope.merge() {
224 val emitter = MutableEvents<Int>()
225 val fizz = emitter.mapNotNull { if (it % 3 == 0) "Fizz" else null }
226 val buzz = emitter.mapNotNull { if (it % 5 == 0) "Buzz" else null }
227 val fizzbuzz = fizz.mergeWith(buzz) { _, _ -> "Fizz Buzz" }
228 val output = mergeLeft(fizzbuzz, emitter.mapCheap { it.toString() })
229
230 var observedOutput: String? = null
231 output.observe { observedOutput = it }
232
233 launchEffect {
234 emitter.emit(1)
235 assertEquals(observedOutput, "1")
236 emitter.emit(2)
237 assertEquals(observedOutput, "2")
238 emitter.emit(3)
239 assertEquals(observedOutput, "Fizz")
240 emitter.emit(4)
241 assertEquals(observedOutput, "4")
242 emitter.emit(5)
243 assertEquals(observedOutput, "Buzz")
244 emitter.emit(6)
245 assertEquals(observedOutput, "Fizz")
246 emitter.emit(15)
247 assertEquals(observedOutput, "Fizz Buzz")
248 }
249 }
250
251 @Test fun test_groupByKey() = runSample { groupByKey() }
252
253 fun BuildScope.groupByKey() {
254 val emitter = MutableEvents<Map<String, Int>>()
255 val grouped = emitter.groupByKey()
256 val groupA = grouped["A"]
257 val groupB = grouped["B"]
258
259 var numEmissions = 0
260 emitter.observe { numEmissions++ }
261
262 var observedA: Int? = null
263 groupA.observe { observedA = it }
264
265 var observedB: Int? = null
266 groupB.observe { observedB = it }
267
268 launchEffect {
269 // emit to group A
270 emitter.emit(mapOf("A" to 3))
271 assertEquals(numEmissions, 1)
272 assertEquals(observedA, 3)
273 assertEquals(observedB, null)
274
275 // emit to groups B and C, even though there are no observers of C
276 emitter.emit(mapOf("B" to 9, "C" to 100))
277 assertEquals(numEmissions, 2)
278 assertEquals(observedA, 3)
279 assertEquals(observedB, 9)
280
281 // emit to groups A and B
282 emitter.emit(mapOf("B" to 6, "A" to 14))
283 assertEquals(numEmissions, 3)
284 assertEquals(observedA, 14)
285 assertEquals(observedB, 6)
286
287 // emit to group with no listeners
288 emitter.emit(mapOf("Q" to -66))
289 assertEquals(numEmissions, 4)
290 assertEquals(observedA, 14)
291 assertEquals(observedB, 6)
292
293 // no-op emission
294 emitter.emit(emptyMap())
295 assertEquals(numEmissions, 5)
296 assertEquals(observedA, 14)
297 assertEquals(observedB, 6)
298 }
299 }
300
301 @Test fun test_switchEvents() = runSample { switchEvents() }
302
303 fun BuildScope.switchEvents() {
304 val negator = MutableEvents<Unit>()
305 val emitter = MutableEvents<Int>()
306 val negate = negator.foldState(false) { _, negate -> !negate }
307 val output =
308 negate.map { negate -> if (negate) emitter.map { it * -1 } else emitter }.switchEvents()
309
310 var observed: Int? = null
311 output.observe { observed = it }
312
313 launchEffect {
314 // emit like normal
315 emitter.emit(10)
316 assertEquals(observed, 10)
317
318 // enable negation
319 observed = null
320 negator.emit(Unit)
321 assertEquals(observed, null)
322
323 emitter.emit(99)
324 assertEquals(observed, -99)
325
326 // disable negation
327 observed = null
328 negator.emit(Unit)
329 emitter.emit(7)
330 assertEquals(observed, 7)
331 }
332 }
333
334 @Test fun test_switchEventsPromptly() = runSample { switchEventsPromptly() }
335
336 fun BuildScope.switchEventsPromptly() {
337 val emitter = MutableEvents<Int>()
338 val enabled = emitter.map { it > 10 }.holdState(false)
339 val switchedIn = enabled.map { enabled -> if (enabled) emitter else emptyEvents }
340 val deferredSwitch = switchedIn.switchEvents()
341 val promptSwitch = switchedIn.switchEventsPromptly()
342
343 var observedDeferred: Int? = null
344 deferredSwitch.observe { observedDeferred = it }
345
346 var observedPrompt: Int? = null
347 promptSwitch.observe { observedPrompt = it }
348
349 launchEffect {
350 emitter.emit(3)
351 assertEquals(observedDeferred, null)
352 assertEquals(observedPrompt, null)
353
354 emitter.emit(20)
355 assertEquals(observedDeferred, null)
356 assertEquals(observedPrompt, 20)
357
358 emitter.emit(30)
359 assertEquals(observedDeferred, 30)
360 assertEquals(observedPrompt, 30)
361
362 emitter.emit(8)
363 assertEquals(observedDeferred, 8)
364 assertEquals(observedPrompt, 8)
365
366 emitter.emit(1)
367 assertEquals(observedDeferred, 8)
368 assertEquals(observedPrompt, 8)
369 }
370 }
371
372 @Test fun test_sampleTransactional() = runSample { sampleTransactional() }
373
374 fun BuildScope.sampleTransactional() {
375 var store = 0
376 val transactional = transactionally { store++ }
377
378 effect {
379 assertEquals(store, 0)
380 assertEquals(transactional.sample(), 0)
381 assertEquals(store, 1)
382 assertEquals(transactional.sample(), 0)
383 assertEquals(store, 1)
384 }
385 }
386
387 @Test fun test_states() = runSample { states() }
388
389 fun BuildScope.states() {
390 val constantState = stateOf(10)
391 effect { assertEquals(constantState.sample(), 10) }
392
393 val mappedConstantState: State<Int> = constantState.map { it * 2 }
394 effect { assertEquals(mappedConstantState.sample(), 20) }
395
396 val emitter = MutableEvents<Int>()
397 val heldState: State<Int?> = emitter.holdState(null)
398 effect { assertEquals(heldState.sample(), null) }
399
400 var observed: Int? = null
401 var wasObserved = false
402 heldState.observe {
403 observed = it
404 wasObserved = true
405 }
406 launchEffect {
407 assertTrue(wasObserved)
408 emitter.emit(4)
409 assertEquals(observed, 4)
410 }
411
412 val combinedStates: State<Pair<Int, Int?>> =
413 combine(mappedConstantState, heldState) { a, b -> Pair(a, b) }
414
415 effect { assertEquals(combinedStates.sample(), 20 to null) }
416
417 var observedPair: Pair<Int, Int?>? = null
418 combinedStates.observe { observedPair = it }
419 launchEffect {
420 emitter.emit(12)
421 assertEquals(observedPair, 20 to 12)
422 }
423 }
424
425 @Test fun test_holdState() = runSample { holdState() }
426
427 fun BuildScope.holdState() {
428 val emitter = MutableEvents<Int>()
429 val heldState: State<Int?> = emitter.holdState(null)
430 effect { assertEquals(heldState.sample(), null) }
431
432 var observed: Int? = null
433 var wasObserved = false
434 heldState.observe {
435 observed = it
436 wasObserved = true
437 }
438 launchEffect {
439 // observation of the initial state took place immediately
440 assertTrue(wasObserved)
441
442 // state changes are also observed
443 emitter.emit(4)
444 assertEquals(observed, 4)
445
446 emitter.emit(20)
447 assertEquals(observed, 20)
448 }
449 }
450
451 @Test fun test_mapState() = runSample { mapState() }
452
453 fun BuildScope.mapState() {
454 val emitter = MutableEvents<Int>()
455 val held: State<Int> = emitter.holdState(0)
456 val squared: State<Int> = held.map { it * it }
457
458 var observed: Int? = null
459 squared.observe { observed = it }
460
461 launchEffect {
462 assertEquals(observed, 0)
463
464 emitter.emit(10)
465 assertEquals(observed, 100)
466 }
467 }
468
469 @Test fun test_combineState() = runSample { combineState() }
470
471 fun BuildScope.combineState() {
472 val emitter = MutableEvents<Int>()
473 val state = emitter.holdState(0)
474 val squared = state.map { it * it }
475 val negated = state.map { -it }
476 val combined = squared.combine(negated) { a, b -> Pair(a, b) }
477
478 val observed = mutableListOf<Pair<Int, Int>>()
479 combined.observe { observed.add(it) }
480
481 launchEffect {
482 emitter.emit(10)
483 emitter.emit(20)
484 emitter.emit(3)
485
486 assertEquals(observed, listOf(0 to 0, 100 to -10, 400 to -20, 9 to -3))
487 }
488 }
489
490 @Test fun test_flatMap() = runSample { flatMap() }
491
492 fun BuildScope.flatMap() {
493 val toggler = MutableEvents<Unit>()
494 val firstEmitter = MutableEvents<Unit>()
495 val secondEmitter = MutableEvents<Unit>()
496
497 val firstCount: State<Int> = firstEmitter.foldState(0) { _, count -> count + 1 }
498 val secondCount: State<Int> = secondEmitter.foldState(0) { _, count -> count + 1 }
499 val toggleState: State<Boolean> = toggler.foldState(true) { _, state -> !state }
500
501 val activeCount: State<Int> =
502 toggleState.flatMap { b -> if (b) firstCount else secondCount }
503
504 var observed: Int? = null
505 activeCount.observe { observed = it }
506
507 launchEffect {
508 assertEquals(observed, 0)
509
510 firstEmitter.emit(Unit)
511 assertEquals(observed, 1)
512
513 secondEmitter.emit(Unit)
514 assertEquals(observed, 1)
515
516 secondEmitter.emit(Unit)
517 assertEquals(observed, 1)
518
519 toggler.emit(Unit)
520 assertEquals(observed, 2)
521
522 toggler.emit(Unit)
523 assertEquals(observed, 1)
524 }
525 }
526
527 @Test fun test_incrementals() = runSample { incrementals() }
528
529 fun BuildScope.incrementals() {
530 val patchEmitter = MutableEvents<MapPatch<String, Int>>()
531 val incremental: Incremental<String, Int> = patchEmitter.foldStateMapIncrementally()
532 val squared = incremental.mapValues { (key, value) -> value * value }
533
534 var observedUpdate: MapPatch<String, Int>? = null
535 squared.updates.observe { observedUpdate = it }
536
537 var observedState: Map<String, Int>? = null
538 squared.observe { observedState = it }
539
540 launchEffect {
541 assertEquals(observedState, emptyMap())
542 assertEquals(observedUpdate, null)
543
544 // add entry: A => 10
545 patchEmitter.emit(mapOf("A" to maybeOf(10)))
546 assertEquals(observedState, mapOf("A" to 100))
547 assertEquals(observedUpdate, mapOf("A" to maybeOf(100)))
548
549 // update entry: A => 5
550 // add entry: B => 6
551 patchEmitter.emit(mapOf("A" to maybeOf(5), "B" to maybeOf(6)))
552 assertEquals(observedState, mapOf("A" to 25, "B" to 36))
553 assertEquals(observedUpdate, mapOf("A" to maybeOf(25), "B" to maybeOf(36)))
554
555 // remove entry: A
556 // add entry: C => 9
557 // remove non-existent entry: F
558 patchEmitter.emit(mapOf("A" to maybeOf(), "C" to maybeOf(9), "F" to maybeOf()))
559 assertEquals(observedState, mapOf("B" to 36, "C" to 81))
560 // non-existent entry is filtered from the update
561 assertEquals(observedUpdate, mapOf("A" to maybeOf(), "C" to maybeOf(81)))
562 }
563 }
564
565 @Test fun test_mergeEventsIncrementally() = runSample(block = mergeEventsIncrementally())
566
567 fun mergeEventsIncrementally(): BuildSpec<Unit> = buildSpec {
568 val patchEmitter = MutableEvents<MapPatch<String, Events<Int>>>()
569 val incremental: Incremental<String, Events<Int>> = patchEmitter.foldStateMapIncrementally()
570 val merged: Events<Map<String, Int>> = incremental.mergeEventsIncrementally()
571
572 var observed: Map<String, Int>? = null
573 merged.observe { observed = it }
574
575 launchEffect {
576 // add events entry: A
577 val emitterA = MutableEvents<Int>()
578 patchEmitter.emit(mapOf("A" to maybeOf(emitterA)))
579
580 emitterA.emit(100)
581 assertEquals(observed, mapOf("A" to 100))
582
583 // add events entry: B
584 val emitterB = MutableEvents<Int>()
585 patchEmitter.emit(mapOf("B" to maybeOf(emitterB)))
586
587 // merged emits from both A and B
588 emitterB.emit(5)
589 assertEquals(observed, mapOf("B" to 5))
590
591 emitterA.emit(20)
592 assertEquals(observed, mapOf("A" to 20))
593
594 // remove entry: A
595 patchEmitter.emit(mapOf("A" to maybeOf()))
596 emitterA.emit(0)
597 // event is not emitted now that A has been removed
598 assertEquals(observed, mapOf("A" to 20))
599
600 // but B still works
601 emitterB.emit(3)
602 assertEquals(observed, mapOf("B" to 3))
603 }
604 }
605
606 @Test
607 fun test_mergeEventsIncrementallyPromptly() =
608 runSample(block = mergeEventsIncrementallyPromptly())
609
610 fun mergeEventsIncrementallyPromptly(): BuildSpec<Unit> = buildSpec {
611 val patchEmitter = MutableEvents<MapPatch<String, Events<Int>>>()
612 val incremental: Incremental<String, Events<Int>> = patchEmitter.foldStateMapIncrementally()
613 val deferredMerge: Events<Map<String, Int>> = incremental.mergeEventsIncrementally()
614 val promptMerge: Events<Map<String, Int>> = incremental.mergeEventsIncrementallyPromptly()
615
616 var observedDeferred: Map<String, Int>? = null
617 deferredMerge.observe { observedDeferred = it }
618
619 var observedPrompt: Map<String, Int>? = null
620 promptMerge.observe { observedPrompt = it }
621
622 launchEffect {
623 val emitterA = MutableEvents<Int>()
624 patchEmitter.emit(mapOf("A" to maybeOf(emitterA)))
625
626 emitterA.emit(100)
627 assertEquals(observedDeferred, mapOf("A" to 100))
628 assertEquals(observedPrompt, mapOf("A" to 100))
629
630 val emitterB = patchEmitter.map { 5 }
631 patchEmitter.emit(mapOf("B" to maybeOf(emitterB)))
632
633 assertEquals(observedDeferred, mapOf("A" to 100))
634 assertEquals(observedPrompt, mapOf("B" to 5))
635 }
636 }
637
638 @Test fun test_applyLatestStateful() = runSample(block = applyLatestStateful())
639
640 fun applyLatestStateful(): BuildSpec<Unit> = buildSpec {
641 val reset = MutableEvents<Unit>()
642 val emitter = MutableEvents<Unit>()
643 val stateEvents: Events<State<Int>> =
644 reset
645 .map { statefully { emitter.foldState(0) { _, count -> count + 1 } } }
646 .applyLatestStateful()
647 val activeState: State<State<Int>?> = stateEvents.holdState(null)
648
649 launchEffect {
650 // nothing is active yet
651 kairosNetwork.transact { assertEquals(activeState.sample(), null) }
652
653 // activate the counter
654 reset.emit(Unit)
655 val firstState =
656 kairosNetwork.transact {
657 assertEquals(activeState.sample()?.sample(), 0)
658 activeState.sample()!!
659 }
660
661 // emit twice
662 emitter.emit(Unit)
663 emitter.emit(Unit)
664 kairosNetwork.transact { assertEquals(firstState.sample(), 2) }
665
666 // start a new counter, disabling the old one
667 reset.emit(Unit)
668 val secondState =
669 kairosNetwork.transact {
670 assertEquals(activeState.sample()?.sample(), 0)
671 activeState.sample()!!
672 }
673 kairosNetwork.transact { assertEquals(firstState.sample(), 2) }
674
675 // emit: the new counter updates, but the old one does not
676 emitter.emit(Unit)
677 kairosNetwork.transact { assertEquals(secondState.sample(), 1) }
678 kairosNetwork.transact { assertEquals(firstState.sample(), 2) }
679 }
680 }
681
682 @Test fun test_applyLatestStatefulForKey() = runSample(block = applyLatestStatefulForKey())
683
684 fun applyLatestStatefulForKey(): BuildSpec<Unit> = buildSpec {
685 val reset = MutableEvents<String>()
686 val emitter = MutableEvents<String>()
687 val stateEvents: Events<MapPatch<String, State<Int>>> =
688 reset
689 .map { key ->
690 mapOf(
691 key to
692 maybeOf(
693 statefully {
694 emitter
695 .filter { it == key }
696 .foldState(0) { _, count -> count + 1 }
697 }
698 )
699 )
700 }
701 .applyLatestStatefulForKey()
702 val activeStatesByKey: Incremental<String, State<Int>> =
703 stateEvents.foldStateMapIncrementally(emptyMap())
704
705 launchEffect {
706 // nothing is active yet
707 kairosNetwork.transact { assertEquals(activeStatesByKey.sample(), emptyMap()) }
708
709 // activate a new entry A
710 reset.emit("A")
711 val firstStateA =
712 kairosNetwork.transact {
713 val stateMap: Map<String, State<Int>> = activeStatesByKey.sample()
714 assertEquals(stateMap.keys, setOf("A"))
715 stateMap.getValue("A").also { assertEquals(it.sample(), 0) }
716 }
717
718 // emit twice to A
719 emitter.emit("A")
720 emitter.emit("A")
721 kairosNetwork.transact { assertEquals(firstStateA.sample(), 2) }
722
723 // active a new entry B
724 reset.emit("B")
725 val firstStateB =
726 kairosNetwork.transact {
727 val stateMap: Map<String, State<Int>> = activeStatesByKey.sample()
728 assertEquals(stateMap.keys, setOf("A", "B"))
729 stateMap.getValue("B").also {
730 assertEquals(it.sample(), 0)
731 assertEquals(firstStateA.sample(), 2)
732 }
733 }
734
735 // emit once to B
736 emitter.emit("B")
737 kairosNetwork.transact {
738 assertEquals(firstStateA.sample(), 2)
739 assertEquals(firstStateB.sample(), 1)
740 }
741
742 // activate a new entry for A, disabling the old entry
743 reset.emit("A")
744 val secondStateA =
745 kairosNetwork.transact {
746 val stateMap: Map<String, State<Int>> = activeStatesByKey.sample()
747 assertEquals(stateMap.keys, setOf("A", "B"))
748 stateMap.getValue("A").also {
749 assertEquals(it.sample(), 0)
750 assertEquals(firstStateB.sample(), 1)
751 }
752 }
753
754 // emit to A: the new A state updates, but the old one does not
755 emitter.emit("A")
756 kairosNetwork.transact {
757 assertEquals(firstStateA.sample(), 2)
758 assertEquals(secondStateA.sample(), 1)
759 }
760 }
761 }
762
763 private fun runSample(
764 dispatcher: TestDispatcher = UnconfinedTestDispatcher(),
765 block: BuildScope.() -> Unit,
766 ) {
767 runTest(dispatcher, timeout = 1.seconds) {
768 val kairosNetwork = backgroundScope.launchKairosNetwork()
769 backgroundScope.launch { kairosNetwork.activateSpec { block() } }
770 }
771 }
772 }
773
assertEqualsnull774 private fun <T> assertEquals(actual: T, expected: T) = Assert.assertEquals(expected, actual)
775