| /* |
| * Copyright 2019 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 |
| |
| import androidx.compose.runtime.mock.EmptyApplier |
| import androidx.compose.runtime.mock.Text |
| import androidx.compose.runtime.mock.View |
| import androidx.compose.runtime.mock.ViewApplier |
| import androidx.compose.runtime.mock.compositionTest |
| import androidx.compose.runtime.mock.expectChanges |
| import androidx.compose.runtime.snapshots.Snapshot |
| import androidx.compose.runtime.snapshots.SnapshotStateObserver |
| import java.lang.ref.WeakReference |
| import kotlin.concurrent.thread |
| import kotlin.test.AfterTest |
| import kotlin.test.BeforeTest |
| import kotlin.test.Test |
| import kotlin.test.assertEquals |
| import kotlin.test.assertNotNull |
| import kotlin.test.assertNull |
| import kotlinx.coroutines.ExperimentalCoroutinesApi |
| import kotlinx.coroutines.delay |
| import kotlinx.coroutines.test.UnconfinedTestDispatcher |
| import kotlinx.coroutines.test.runTest |
| |
| @Stable |
| @OptIn(InternalComposeApi::class) |
| @Suppress("unused") |
| class JvmCompositionTests { |
| // Regression test for b/202967533 |
| // Test taken from the bug report; reformatted to conform to lint rules. |
| @OptIn(ExperimentalCoroutinesApi::class) |
| @Test |
| fun avoidsDeadlockInRecomposerComposerDispose() { |
| val thread = thread { |
| while (!Thread.interrupted()) { |
| // -> synchronized(stateLock) -> recordComposerModificationsLocked |
| // -> composition.recordModificationsOf -> synchronized(lock) |
| Snapshot.sendApplyNotifications() |
| } |
| } |
| |
| for (i in 1..1000) { |
| runTest(UnconfinedTestDispatcher()) { |
| localRecomposerTest { |
| @Suppress("ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE") |
| var value by mutableStateOf(0) |
| val snapshotObserver = SnapshotStateObserver {} |
| snapshotObserver.start() |
| @Suppress("UNUSED_VALUE") |
| value = 4 |
| val composition = Composition(EmptyApplier(), it) |
| composition.setContent {} |
| |
| // -> synchronized(lock) -> parent.unregisterComposition(this) |
| // -> synchronized(stateLock) |
| composition.dispose() |
| snapshotObserver.stop() |
| } |
| } |
| } |
| |
| thread.interrupt() |
| } |
| |
| @Test |
| @OptIn(ExperimentalCoroutinesApi::class) |
| fun avoidRaceConditionWhenInvalidating() = compositionTest { |
| var scope: RecomposeScope? = null |
| var count = 0 |
| var threadException: Exception? = null |
| val thread = thread { |
| try { |
| while (!Thread.interrupted()) { |
| scope?.invalidate() |
| count++ |
| } |
| } catch (e: Exception) { |
| threadException = e |
| } |
| } |
| |
| compose { |
| scope = currentRecomposeScope |
| Text("Some text") |
| Text("Count $count") |
| } |
| |
| repeat(20) { |
| advance(ignorePendingWork = true) |
| delay(1) |
| } |
| |
| thread.interrupt() |
| @Suppress("BlockingMethodInNonBlockingContext") |
| thread.join() |
| delay(10) |
| threadException?.let { throw it } |
| } |
| |
| @Test |
| @OptIn(ExperimentalCoroutinesApi::class) |
| fun avoidRaceConditionWhenApplyingSnapshotsInAThread() = compositionTest { |
| val count = mutableStateOf(0) |
| var threadException: Exception? = null |
| |
| compose { |
| Text("Some text") |
| Text("Count ${count.value}") |
| } |
| |
| val thread = thread { |
| try { |
| while (!Thread.interrupted()) { |
| Snapshot.withMutableSnapshot { |
| count.value++ |
| } |
| } |
| } catch (e: Exception) { |
| threadException = e |
| } |
| } |
| |
| repeat(200) { |
| advance(ignorePendingWork = true) |
| delay(1) |
| } |
| |
| thread.interrupt() |
| @Suppress("BlockingMethodInNonBlockingContext") |
| thread.join() |
| delay(10) |
| threadException?.let { throw it } |
| } |
| |
| @Test // b/197064250 and others |
| fun canInvalidateDuringApplyChanges() = compositionTest { |
| var value by mutableStateOf(0) |
| compose { |
| Wrap { |
| val scope = currentRecomposeScope |
| ComposeNode<View, ViewApplier>( |
| factory = { View().also { it.name = "linear" } }, |
| update = { |
| set(value) { |
| scope.invalidate() |
| this.attributes["value"] = value.toString() |
| } |
| } |
| ) |
| } |
| } |
| |
| value = 2 |
| expectChanges() |
| } |
| |
| @OptIn(ExperimentalCoroutinesApi::class) |
| @Test // Regression test for https://issuetracker.google.com/262356264 |
| fun compositionDoesNotRetainSnapshotReference() = runTest { |
| localRecomposerTest { recomposer -> |
| var compositionSnapshot: WeakReference<Snapshot>? = null |
| val composition = Composition(UnitApplier(), recomposer) |
| composition.setContent { |
| compositionSnapshot = WeakReference(Snapshot.current) |
| } |
| assertNotNull(compositionSnapshot, "compositionSnapshot weak reference") |
| repeat(10) { |
| Runtime.getRuntime().gc() |
| } |
| assertNull(compositionSnapshot?.get(), "weak snapshot reference after forced gc") |
| } |
| } |
| |
| private class TestReference(val invokeCount: Int = 0) : () -> Int { |
| override fun invoke(): Int = invokeCount |
| |
| // overridden equals to test if remember compares this value correctly |
| override fun equals(other: Any?): Boolean { |
| return other is TestReference |
| } |
| } |
| |
| @Composable private fun rememberWFunctionReference(ref: () -> Int): Int { |
| val remembered = remember(ref) { ref() } |
| assertEquals(remembered, 0) |
| return remembered |
| } |
| |
| // regression test for b/319810819 |
| @Test |
| fun remember_functionReference_key() = compositionTest { |
| var state by mutableIntStateOf(0) |
| compose { |
| // use custom ref implementation to avoid strong skipping memoizing the instance |
| rememberWFunctionReference(TestReference(state)) |
| } |
| verifyConsistent() |
| |
| state++ |
| advance() |
| verifyConsistent() |
| } |
| |
| private var count = 0 |
| @BeforeTest fun saveSnapshotCount() { |
| count = Snapshot.openSnapshotCount() |
| } |
| |
| @AfterTest fun checkSnapshotCount() { |
| val afterCount = Snapshot.openSnapshotCount() |
| assertEquals(count, afterCount, "A snapshot was left open after the test") |
| } |
| } |