Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit b11cb492 authored by David Saff's avatar David Saff
Browse files

SceneFrameworkIntegrationTest does not need runCurrent

This does not switch to UnconfinedDispatcher.  Instead, it uses
several idioms to guarantee that runCurrent is called at the right
times:

- currentValue(StateFlow): this is already introduced, but now
  used consistently
- verifyCurrent(mock) for advancing tasks before verifying a mock
- currentValue { block } for clearly scoping other value retrievals
  that require advancement

Note: this accomplishes similar goals to ag/30532049, but with
fewer novel/bold/clever/intrusive patterns.

See go/thetiger for context

Bug: 342622417
Test: SceneFrameworkIntegrationTest
Flag: TEST_ONLY
Change-Id: Ideda9a26eb604ddf5d329ce45f5ebae0e011b3d9
parent 283ce250
Loading
Loading
Loading
Loading
+14 −35
Original line number Diff line number Diff line
@@ -37,7 +37,6 @@ import com.android.systemui.authentication.shared.model.AuthenticationMethodMode
import com.android.systemui.bouncer.ui.viewmodel.PasswordBouncerViewModel
import com.android.systemui.bouncer.ui.viewmodel.PinBouncerViewModel
import com.android.systemui.bouncer.ui.viewmodel.bouncerSceneContentViewModel
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository
import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
import com.android.systemui.flags.EnableSceneContainer
@@ -48,9 +47,9 @@ import com.android.systemui.keyguard.ui.viewmodel.lockscreenUserActionsViewModel
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.collectLastValue
import com.android.systemui.kosmos.currentValue
import com.android.systemui.kosmos.runCurrent
import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.testScope
import com.android.systemui.kosmos.verifyCurrent
import com.android.systemui.lifecycle.activateIn
import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
@@ -77,12 +76,9 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.verify

/**
 * Integration test cases for the Scene Framework.
@@ -137,10 +133,10 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() {
            sceneContainerViewModel.activateIn(testScope)

            assertWithMessage("Initial scene key mismatch!")
                .that(sceneContainerViewModel.currentScene.value)
                .that(currentValue(sceneContainerViewModel.currentScene))
                .isEqualTo(sceneContainerConfig.initialSceneKey)
            assertWithMessage("Initial scene container visibility mismatch!")
                .that(sceneContainerViewModel.isVisible)
                .that(currentValue { sceneContainerViewModel.isVisible })
                .isTrue()
        }

@@ -337,7 +333,6 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() {
                .that(bouncerActionButton)
                .isNotNull()
            kosmos.bouncerSceneContentViewModel.onActionButtonClicked(bouncerActionButton!!)
            runCurrent()

            // TODO(b/369765704): Assert that an activity was started once we use ActivityStarter.
        }
@@ -358,9 +353,8 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() {
                .that(bouncerActionButton)
                .isNotNull()
            kosmos.bouncerSceneContentViewModel.onActionButtonClicked(bouncerActionButton!!)
            runCurrent()

            verify(mockTelecomManager).showInCallScreen(any())
            verifyCurrent(mockTelecomManager).showInCallScreen(any())
        }

    @Test
@@ -413,7 +407,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() {
     * the UI must gradually transition between scenes.
     */
    private fun Kosmos.getCurrentSceneInUi(): SceneKey {
        return when (val state = transitionState.value) {
        return when (val state = currentValue(transitionState)) {
            is ObservableTransitionState.Idle -> state.currentScene
            is ObservableTransitionState.Transition.ChangeScene -> state.fromScene
            is ObservableTransitionState.Transition.ShowOrHideOverlay -> state.currentScene
@@ -436,7 +430,6 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() {
        // is not an observable that can trigger a new evaluation.
        fakeDeviceEntryRepository.setLockscreenEnabled(enableLockscreen)
        fakeAuthenticationRepository.setAuthenticationMethod(authMethod)
        testScope.runCurrent()
    }

    /** Emulates a phone call in progress. */
@@ -447,7 +440,6 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() {
            setIsInCall(true)
            setCallState(TelephonyManager.CALL_STATE_OFFHOOK)
        }
        testScope.runCurrent()
    }

    /**
@@ -480,24 +472,21 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() {
                isInitiatedByUserInput = false,
                isUserInputOngoing = flowOf(false),
            )
        testScope.runCurrent()

        // Report progress of transition.
        while (progressFlow.value < 1f) {
        while (currentValue(progressFlow) < 1f) {
            progressFlow.value += 0.2f
            testScope.runCurrent()
        }

        // End the transition and report the change.
        transitionState.value = ObservableTransitionState.Idle(to)

        fakeSceneDataSource.unpause(force = true)
        testScope.runCurrent()

        assertWithMessage("Visibility mismatch after scene transition from $from to $to!")
            .that(sceneContainerViewModel.isVisible)
            .that(currentValue { sceneContainerViewModel.isVisible })
            .isEqualTo(expectedVisible)
        assertThat(sceneContainerViewModel.currentScene.value).isEqualTo(to)
        assertThat(currentValue(sceneContainerViewModel.currentScene)).isEqualTo(to)

        bouncerSceneJob =
            if (to == Scenes.Bouncer) {
@@ -510,7 +499,6 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() {
                bouncerSceneJob?.cancel()
                null
            }
        testScope.runCurrent()
    }

    /**
@@ -556,13 +544,12 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() {
        )

        powerInteractor.setAwakeForTest()
        testScope.runCurrent()
    }

    /** Unlocks the device by entering the correct PIN. Ends up in the Gone scene. */
    private fun Kosmos.unlockDevice() {
        assertWithMessage("Cannot unlock a device that's already unlocked!")
            .that(deviceEntryInteractor.isUnlocked.value)
            .that(currentValue(deviceEntryInteractor.isUnlocked))
            .isFalse()

        emulateUserDrivenTransition(Scenes.Bouncer)
@@ -595,7 +582,6 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() {
            pinBouncerViewModel.onPinButtonClicked(digit)
        }
        pinBouncerViewModel.onAuthenticateButtonClicked()
        testScope.runCurrent()
    }

    /**
@@ -625,26 +611,23 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() {
        }
        pinBouncerViewModel.onAuthenticateButtonClicked()
        fakeMobileConnectionsRepository.isAnySimSecure.value = false
        testScope.runCurrent()

        setAuthMethod(authMethodAfterSimUnlock, enableLockscreen)
        testScope.runCurrent()
    }

    /** Changes device wakefulness state from asleep to awake, going through intermediary states. */
    private fun Kosmos.wakeUpDevice() {
        val wakefulnessModel = powerInteractor.detailedWakefulness.value
        val wakefulnessModel = currentValue(powerInteractor.detailedWakefulness)
        assertWithMessage("Cannot wake up device as it's already awake!")
            .that(wakefulnessModel.isAwake())
            .isFalse()

        powerInteractor.setAwakeForTest()
        testScope.runCurrent()
    }

    /** Changes device wakefulness state from awake to asleep, going through intermediary states. */
    private suspend fun Kosmos.putDeviceToSleep(waitForLock: Boolean = true) {
        val wakefulnessModel = powerInteractor.detailedWakefulness.value
        val wakefulnessModel = currentValue(powerInteractor.detailedWakefulness)
        assertWithMessage("Cannot put device to sleep as it's already asleep!")
            .that(wakefulnessModel.isAwake())
            .isTrue()
@@ -659,22 +642,18 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() {
                    )
                    .toLong()
            )
        } else {
            testScope.runCurrent()
        }
    }

    /** Emulates the dismissal of the IME (soft keyboard). */
    private fun Kosmos.dismissIme() {
        (bouncerSceneContentViewModel.authMethodViewModel.value as? PasswordBouncerViewModel)?.let {
            it.onImeDismissed()
            testScope.runCurrent()
        }
        (currentValue(bouncerSceneContentViewModel.authMethodViewModel)
                as? PasswordBouncerViewModel)
            ?.let { it.onImeDismissed() }
    }

    private fun Kosmos.introduceLockedSim() {
        setAuthMethod(AuthenticationMethodModel.Sim)
        fakeMobileConnectionsRepository.isAnySimSecure.value = true
        testScope.runCurrent()
    }
}
+27 −0
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@ import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.mockito.kotlin.verify

var Kosmos.testDispatcher by Fixture { StandardTestDispatcher() }

@@ -82,6 +83,32 @@ fun <T> TestScope.currentValue(stateFlow: StateFlow<T>): T {
}

/** Retrieve the current value of this [StateFlow] safely. See `currentValue(TestScope)`. */
fun <T> Kosmos.currentValue(fn: () -> T) = testScope.currentValue(fn)

/**
 * Retrieve the result of [fn] after running all pending tasks. Do not use to retrieve the value of
 * a flow directly; for that, use either `currentValue(StateFlow)` or [collectLastValue]
 */
@OptIn(ExperimentalCoroutinesApi::class)
fun <T> TestScope.currentValue(fn: () -> T): T {
    runCurrent()
    return fn()
}

/** Retrieve the result of [fn] after running all pending tasks. See `TestScope.currentValue(fn)` */
fun <T> Kosmos.currentValue(stateFlow: StateFlow<T>): T {
    return testScope.currentValue(stateFlow)
}

/** Safely verify that a mock has been called after the test scope has caught up */
@OptIn(ExperimentalCoroutinesApi::class)
fun <T> TestScope.verifyCurrent(mock: T): T {
    runCurrent()
    return verify(mock)
}

/**
 * Safely verify that a mock has been called after the test scope has caught up. See
 * `TestScope.verifyCurrent`
 */
fun <T> Kosmos.verifyCurrent(mock: T) = testScope.verifyCurrent(mock)
+21 −22
Original line number Diff line number Diff line
@@ -19,13 +19,13 @@ package com.android.systemui.scene.shared.model
import com.android.compose.animation.scene.OverlayKey
import com.android.compose.animation.scene.SceneKey
import com.android.compose.animation.scene.TransitionKey
import com.android.systemui.kosmos.currentValue
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.test.TestScope

class FakeSceneDataSource(
    initialSceneKey: SceneKey,
) : SceneDataSource {
class FakeSceneDataSource(initialSceneKey: SceneKey, val testScope: TestScope) : SceneDataSource {

    private val _currentScene = MutableStateFlow(initialSceneKey)
    override val currentScene: StateFlow<SceneKey> = _currentScene.asStateFlow()
@@ -33,18 +33,20 @@ class FakeSceneDataSource(
    private val _currentOverlays = MutableStateFlow<Set<OverlayKey>>(emptySet())
    override val currentOverlays: StateFlow<Set<OverlayKey>> = _currentOverlays.asStateFlow()

    var isPaused = false
        private set
    private var _isPaused = false
    val isPaused
        get() = testScope.currentValue { _isPaused }

    var pendingScene: SceneKey? = null
        private set
    private var _pendingScene: SceneKey? = null
    val pendingScene
        get() = testScope.currentValue { _pendingScene }

    var pendingOverlays: Set<OverlayKey>? = null
        private set

    override fun changeScene(toScene: SceneKey, transitionKey: TransitionKey?) {
        if (isPaused) {
            pendingScene = toScene
        if (_isPaused) {
            _pendingScene = toScene
        } else {
            _currentScene.value = toScene
        }
@@ -55,7 +57,7 @@ class FakeSceneDataSource(
    }

    override fun showOverlay(overlay: OverlayKey, transitionKey: TransitionKey?) {
        if (isPaused) {
        if (_isPaused) {
            pendingOverlays = (pendingOverlays ?: currentOverlays.value) + overlay
        } else {
            _currentOverlays.value += overlay
@@ -63,7 +65,7 @@ class FakeSceneDataSource(
    }

    override fun hideOverlay(overlay: OverlayKey, transitionKey: TransitionKey?) {
        if (isPaused) {
        if (_isPaused) {
            pendingOverlays = (pendingOverlays ?: currentOverlays.value) - overlay
        } else {
            _currentOverlays.value -= overlay
@@ -82,9 +84,9 @@ class FakeSceneDataSource(
     * last one will be remembered.
     */
    fun pause() {
        check(!isPaused) { "Can't pause what's already paused!" }
        check(!_isPaused) { "Can't pause what's already paused!" }

        isPaused = true
        _isPaused = true
    }

    /**
@@ -100,15 +102,12 @@ class FakeSceneDataSource(
     *
     * If [expectedScene] is provided, will assert that it's indeed the latest called.
     */
    fun unpause(
        force: Boolean = false,
        expectedScene: SceneKey? = null,
    ) {
        check(force || isPaused) { "Can't unpause what's already not paused!" }

        isPaused = false
        pendingScene?.let { _currentScene.value = it }
        pendingScene = null
    fun unpause(force: Boolean = false, expectedScene: SceneKey? = null) {
        check(force || _isPaused) { "Can't unpause what's already not paused!" }

        _isPaused = false
        _pendingScene?.let { _currentScene.value = it }
        _pendingScene = null
        pendingOverlays?.let { _currentOverlays.value = it }
        pendingOverlays = null

+2 −3
Original line number Diff line number Diff line
@@ -19,13 +19,12 @@ package com.android.systemui.scene.shared.model
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.kosmos.testScope
import com.android.systemui.scene.initialSceneKey
import com.android.systemui.scene.sceneContainerConfig

val Kosmos.fakeSceneDataSource by Fixture {
    FakeSceneDataSource(
        initialSceneKey = initialSceneKey,
    )
    FakeSceneDataSource(initialSceneKey = initialSceneKey, testScope = testScope)
}

val Kosmos.sceneDataSourceDelegator by Fixture {