blob: 3457ab26778a6b034f2506833c13f9a74cb33abd [file] [log] [blame]
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:OptIn(InternalComposeApi::class)
package androidx.compose.runtime.snapshots
import androidx.compose.runtime.InternalComposeApi
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.Snapshot.Companion.openSnapshotCount
import kotlin.test.Ignore
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
class DerivedSnapshotStateTests {
@Test
fun aStateFreeCalculationCanBeUsed() {
val a = derivedStateOf { 10 }
assertEquals(10, a.value)
}
@Test
fun theCalculationIsCached() {
var runs = 0
var i = 0
val a = derivedStateOf { runs++; i }
assertEquals(0, runs, "The calculation is run only when the value is first requested")
i++
assertEquals(1, a.value, "The calculation is run only when the value is first requested")
i++
assertEquals(1, a.value, "The calculation is run only once")
}
@Test
fun statesCanBeUsedInGlobalSnapshot() {
val a = mutableStateOf(1)
val b = mutableStateOf(10)
val c = derivedStateOf { a.value + b.value }
assertEquals(11, c.value)
a.value += 1
assertEquals(12, c.value)
b.value += 10
assertEquals(22, c.value)
}
@Test
fun statesCanBeUsedInSnapshot() {
val a = mutableStateOf(1)
val b = mutableStateOf(10)
val c = derivedStateOf { a.value + b.value }
val snapshot = Snapshot.takeMutableSnapshot()
try {
assertEquals(11, c.value)
a.value += 1
assertEquals(12, c.value)
b.value += 10
assertEquals(22, c.value)
} finally {
snapshot.dispose()
}
}
@Test
fun snapshotsAreIsolatedFromGlobalChanges() {
var state by mutableStateOf(0)
val derived by derivedStateOf { state }
val snapshot = Snapshot.takeSnapshot()
try {
state = 1
assertEquals(1, state)
assertEquals(1, derived)
assertEquals(0, snapshot.enter { state })
assertEquals(0, snapshot.enter { derived })
} finally {
snapshot.dispose()
}
}
@Test
fun mutableSnapshotsCanBeApplied() {
var state by mutableStateOf(0)
val derived by derivedStateOf { state }
val snapshot = Snapshot.takeMutableSnapshot()
try {
snapshot.enter {
assertEquals(0, state)
assertEquals(0, derived)
state = 1
assertEquals(1, state)
assertEquals(1, derived)
}
assertEquals(0, state)
assertEquals(0, derived)
snapshot.apply().check()
assertEquals(1, state)
assertEquals(1, derived)
} finally {
snapshot.dispose()
}
// The same thing can be done with an atomic block
atomic {
assertEquals(1, state)
assertEquals(1, derived)
state = 2
assertEquals(2, state)
assertEquals(2, derived)
}
assertEquals(2, state)
assertEquals(2, derived)
}
@Test
@Ignore // "b/169406779: Flaky test"
fun multipleSnapshotsAreIsolatedAndCanBeApplied() {
val count = 2
val state = MutableList(count) { mutableStateOf(0) }
val derived = state.map { derivedStateOf { it.value } }
// Create count snapshots
val snapshots = MutableList(count) { Snapshot.takeMutableSnapshot() }
try {
repeat(count) {
assertEquals(0, state[it].value)
assertEquals(0, derived[it].value)
}
snapshots.forEachIndexed { index, snapshot ->
snapshot.enter { state[index].value = index }
}
// Ensure the modifications in snapshots are not visible to global
repeat(count) {
assertEquals(0, state[it].value)
assertEquals(0, derived[it].value)
}
// Ensure snapshots can see their own value but no other changes
repeat(count) { index ->
snapshots[index].enter {
repeat(count) {
if (it != index) assertEquals(0, state[it].value)
else assertEquals(it, state[it].value)
if (it != index) assertEquals(0, derived[it].value)
else assertEquals(it, derived[it].value)
}
}
}
// Apply all the snapshots
repeat(count) {
snapshots[it].apply().check()
}
// Global should now be able to see all changes
repeat(count) {
assertEquals(it, state[it].value)
assertEquals(it, derived[it].value)
}
} finally {
// Dispose the snapshots
snapshots.forEach { it.dispose() }
}
}
@Test
fun stateReadsCanBeObserved() {
val state = mutableStateOf(0)
val derived = derivedStateOf { state.value }
var readCount = 0
val readStates = mutableSetOf<Any>()
val snapshot = Snapshot.takeSnapshot {
readCount++
readStates.add(it)
}
try {
val result = snapshot.enter { derived.value }
assertEquals(0, result)
// 1 for derived, 1 for state
assertEquals(2, readStates.size)
// 1 for derived, 1 for state
assertEquals(2, readCount)
assertEquals(true, readStates.contains(state))
assertEquals(true, readStates.contains(derived))
} finally {
snapshot.dispose()
}
}
@Test
fun stateReadsCanBeObservedEvenIfCached() {
val state = mutableStateOf(0)
val derived = derivedStateOf { state.value }
assertEquals(0, derived.value)
val readStates = mutableListOf<Any>()
val snapshot = Snapshot.takeSnapshot {
readStates.add(it)
}
try {
val result = snapshot.enter { derived.value }
assertEquals(0, result)
// 1 for derived, 1 for state
assertEquals(2, readStates.size)
assertEquals(derived, readStates[0])
assertEquals(state, readStates[1])
} finally {
snapshot.dispose()
}
}
@Test
fun nullResultIsCached() {
var runs = 0
val a = derivedStateOf { runs++; null }
assertNull(a.value)
assertEquals(1, runs)
assertNull(a.value)
assertEquals(1, runs)
}
@Test
fun stateUpdatedInSnapshotIsNotRecalculated() {
var runs = 0
val dependency = mutableStateOf(0)
val a = derivedStateOf {
runs++
dependency.value
}
val b = derivedStateOf {
a.value
}
Snapshot.takeMutableSnapshot().apply {
enter {
b.value
}
apply()
}
dependency.value++
Snapshot.takeMutableSnapshot().apply {
enter {
b.value
}
apply()
}
Snapshot.takeMutableSnapshot().apply {
enter {
b.value
}
apply()
}
assertEquals(2, runs)
}
private var count = 0
@BeforeTest
fun recordOpenSnapshots() {
count = openSnapshotCount()
}
// Validate that the tests do not change the number of open snapshots
@AfterTest
fun validateOpenSnapshots() {
assertEquals(count, openSnapshotCount())
}
}