blob: 3e04f55b4a44be968acb7dc2afd9af4489b82d01 [file] [log] [blame]
/*
* Copyright 2022 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.
*/
package androidx.compose.runtime
import androidx.compose.material.Text
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import com.google.common.truth.Truth.assertThat
import org.junit.After
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
var someData = mutableStateOf(0)
@OptIn(InternalComposeApi::class)
@RunWith(AndroidJUnit4::class)
class LiveEditApiTests : BaseComposeTest() {
@get:Rule
override val activityRule = makeTestActivityRule()
private fun invalidateGroup(key: Int) {
invalidateGroupsWithKey(key)
}
private fun compositionErrors(): List<Pair<Exception, Boolean>> =
currentCompositionErrors()
@Before
fun setUp() {
// ensures recomposer knows that hot reload is on
invalidateGroupsWithKey(-1)
}
@After
fun tearDown() {
clearCompositionErrors()
}
// IMPORTANT: This must be the first test as the lambda key will change if the lambda is
// moved this file.
@Test
@MediumTest
fun forceRecompose_setContentLambda() {
var setContentLambdaInvoked = 0
val setContentLambdaKey = -1216916760 // Extracted from .class file (see note above)
activity.show {
Text("Some text")
setContentLambdaInvoked++
}
activity.waitForAFrame()
val setContentLambdaStart = setContentLambdaInvoked
invalidateGroup(setContentLambdaKey)
activity.waitForAFrame()
assertTrue(
"show's lambda should have been invoked",
setContentLambdaInvoked > setContentLambdaStart
)
}
@Test
@MediumTest
fun forceRecompose_Simple() {
val activity = activityRule.activity
activity.show {
TestSimple()
}
activity.waitForAFrame()
val someFunctionStart = someFunctionInvoked
val nestedContentStart = nestedContentInvoked
invalidateGroup(someFunctionKey)
activity.waitForAFrame()
assertTrue(
"SomeFunction should have been invoked",
someFunctionInvoked > someFunctionStart
)
assertTrue(
"NestedContent should have been invoked",
nestedContentInvoked > nestedContentStart
)
}
@Test
@MediumTest
fun forceRecompose_NonRestartable() {
val activity = activityRule.activity
activity.show {
TestNonRestartable()
}
activity.waitForAFrame()
val nonRestartableStart = nonRestartableInvoked
invalidateGroup(nonRestartableKey)
activity.waitForAFrame()
assertTrue(
"NonRestartable should have been invoked",
nonRestartableInvoked > nonRestartableStart
)
}
@Test
@MediumTest
fun forceRecompose_ReadOnly() {
activity.show { TestReadOnly() }
activity.waitForAFrame()
repeat(3) {
val readOnlyStart = readOnlyInvoked
invalidateGroup(readOnlyKey)
activity.waitForAFrame()
assertTrue(
"ReadOnly should have been invoked, iteration $it",
readOnlyInvoked > readOnlyStart
)
}
}
@Test
@MediumTest
fun forceRecompose_NonRestartableWrapper() {
activity.show {
TestNonRestartWrapper()
}
activity.waitForAFrame()
// Ensure that scopes recomposable so the "shouldn't execute" checks below are correct
invalidateGroup(nonRestartableKey)
activity.waitForAFrame()
// Invalidate restart
run {
val nonRestartableStart = nonRestartableInvoked
val nonRestartWrapperStart = nonRestartWrapperInvoked
invalidateGroup(nonRestartableKey)
activity.waitForAFrame()
assertTrue(
"NonRestartable should have been invoked",
nonRestartableInvoked > nonRestartableStart
)
assertTrue(
"NonRestartWrapper invoked when it shouldn't have been",
nonRestartWrapperStart == nonRestartWrapperInvoked
)
}
// Invalidate the wrapper
run {
val nonRestartableStart = nonRestartableInvoked
val nonRestartWrapperStart = nonRestartWrapperInvoked
invalidateGroup(nonRestartWrapperKey)
activity.waitForAFrame()
assertTrue(
"NonRestartable should have been invoked",
nonRestartableInvoked > nonRestartableStart
)
assertTrue(
"NonRestartWrapper should have been invoked",
nonRestartWrapperInvoked > nonRestartWrapperStart
)
}
}
@Test
@MediumTest
fun throwError_doesntCrash() {
activity.show {
TestError()
}
activity.waitForAFrame()
// Invalidate error scope
run {
val errorStart = errorInvoked
invalidateGroup(errorKey)
assertTrue(
"TestError should have been invoked",
errorInvoked > errorStart
)
}
}
@Test
@MediumTest
fun throwError_invalidatesOnlyAfterHotReloadCall() {
val shouldThrow = mutableStateOf(true)
activity.show {
TestError { shouldThrow.value }
}
activity.waitForAFrame()
run {
val errorStart = errorInvoked
shouldThrow.value = false
activity.waitForAFrame()
assertTrue(
"TestError should not have been invoked",
errorInvoked == errorStart
)
invalidateGroup(errorKey)
assertTrue(
"TestError should have been invoked",
errorInvoked > errorStart
)
}
}
@Test
@MediumTest
fun throwError_recompose_doesntCrash() {
val shouldThrow = mutableStateOf(false)
activity.show {
TestError { shouldThrow.value }
}
activity.waitForAFrame()
run {
var errors = compositionErrors()
assertThat(errors).isEmpty()
shouldThrow.value = true
activity.waitForAFrame()
errors = compositionErrors()
assertThat(errors).hasSize(1)
assertThat(errors[0].first.message).isEqualTo("Test crash!")
assertThat(errors[0].second).isEqualTo(true)
}
}
@Test
@MediumTest
fun throwError_recompose_clearErrorOnInvalidate() {
var shouldThrow by mutableStateOf(false)
activity.show {
TestError { shouldThrow }
}
activity.waitForAFrame()
run {
var errors = compositionErrors()
assertThat(errors).isEmpty()
shouldThrow = true
activity.waitForAFrame()
errors = compositionErrors()
assertThat(errors).hasSize(1)
shouldThrow = false
invalidateGroupsWithKey(errorKey)
errors = compositionErrors()
assertThat(errors).isEmpty()
}
}
@Test
@MediumTest
fun throwError_returnsCurrentError() {
var shouldThrow by mutableStateOf(true)
activity.show {
TestError { shouldThrow }
}
activity.waitForAFrame()
run {
var errors = compositionErrors()
assertThat(errors).hasSize(1)
assertThat(errors[0].first.message).isEqualTo("Test crash!")
assertThat(errors[0].second).isEqualTo(true)
shouldThrow = false
invalidateGroup(errorKey)
errors = compositionErrors()
assertThat(errors).isEmpty()
}
}
@Test
@MediumTest
fun throwErrorInEffect_doesntCrash() {
activity.show {
TestEffectError()
}
activity.waitForAFrame()
run {
var errors = compositionErrors()
assertThat(errors).hasSize(1)
assertThat(errors[0].first.message).isEqualTo("Effect error!")
assertThat(errors[0].second).isEqualTo(false)
val start = effectErrorInvoked
simulateHotReload(Unit)
assertTrue("TestEffectError should be invoked!", effectErrorInvoked > start)
errors = compositionErrors()
assertThat(errors).hasSize(1)
assertThat(errors[0].first.message).isEqualTo("Effect error!")
assertThat(errors[0].second).isEqualTo(false)
}
}
@Test
@MediumTest
fun throwErrorInEffect_doesntRecoverOnInvalidate() {
var shouldThrow = true
activity.show {
TestEffectError { shouldThrow }
}
activity.waitForAFrame()
run {
val start = effectErrorInvoked
val errors = compositionErrors()
assertThat(errors).hasSize(1)
assertThat(errors[0].first.message).isEqualTo("Effect error!")
assertThat(errors[0].second).isEqualTo(false)
shouldThrow = false
invalidateGroup(effectErrorKey)
assertTrue("TestEffectError should not be invoked!", effectErrorInvoked == start)
}
}
@Test
@MediumTest
fun throwErrorInEffect_recoversOnReload() {
var shouldThrow = true
activity.show {
TestEffectError { shouldThrow }
}
activity.waitForAFrame()
run {
val start = effectErrorInvoked
var errors = compositionErrors()
assertThat(errors).hasSize(1)
assertThat(errors[0].first.message).isEqualTo("Effect error!")
assertThat(errors[0].second).isEqualTo(false)
shouldThrow = false
simulateHotReload(Unit)
assertTrue("TestEffectError should be invoked!", effectErrorInvoked > start)
errors = compositionErrors()
assertThat(errors).hasSize(0)
}
}
}
const val someFunctionKey = -1580285603 // Extracted from .class file
var someFunctionInvoked = 0
@Composable
fun SomeFunction(a: Int) {
Text("a = $a, someData = ${someData.value}")
NestedContent()
someFunctionInvoked++
}
const val nestedContentKey = 1771808426 // Extracted from .class file
var nestedContentInvoked = 0
@Composable
fun NestedContent() {
Text("Some nested content: ${someData.value}")
nestedContentInvoked++
}
const val nonRestartableKey = 1860384 // Extracted from .class file
var nonRestartableInvoked = 0
@Composable
@NonRestartableComposable
fun NonRestartable() {
Text("Non restart")
nonRestartableInvoked++
}
const val nonRestartWrapperKey = 1287143243 // Extracted from .class file
var nonRestartWrapperInvoked = 0
@Composable
@NonRestartableComposable
fun NonRestartWrapper(block: @Composable () -> Unit) {
Text("Before")
block()
Text("After")
nonRestartWrapperInvoked++
}
const val restartableWrapperKey = -153795690 // Extracted from .class file
var restartWrapperInvoked = 0
@Composable
fun RestartableWrapper(block: @Composable () -> Unit) {
Text("Before")
block()
Text("After")
restartWrapperInvoked++
}
const val readOnlyKey = -1414835162 // Extracted from .class file
var readOnlyInvoked = 0
@Composable
@ReadOnlyComposable
fun ReadOnly() {
readOnlyInvoked++
}
// Test functions
@Composable
fun TestSimple() {
Text("This is some text")
SomeFunction(21)
}
@Composable
fun TestNonRestartable() {
NonRestartable()
}
@Composable
fun TestNonRestartWrapper() {
NonRestartWrapper {
NonRestartable()
}
NestedContent()
}
@Composable
fun TestReadOnly() {
ReadOnly()
}
@Composable
fun TestReadOnlyNested() {
RestartableWrapper {
ReadOnly()
}
}
private const val errorKey = -0x3d6d007a // Extracted from .class file
private var errorInvoked = 0
@Composable
fun TestError(shouldThrow: () -> Boolean = { true }) {
errorInvoked++
if (shouldThrow()) {
error("Test crash!")
}
}
private const val effectErrorKey = -0x43852062 // Extracted from .class file
private var effectErrorInvoked = 0
@Composable
fun TestEffectError(shouldThrow: () -> Boolean = { true }) {
effectErrorInvoked++
SideEffect {
if (shouldThrow()) {
error("Effect error!")
}
}
}